` ).join(""); return t ? `${r}` : r; }, }, }, flagEnabled: s.U5, codec: { encode: s.hD, decode: s.P_ }, }), inUse: !1, }), f.push(t)) : ((0, s.U5)("rewriterLogs", e.base) && console.log( `using cached rewriter ${r} from list of ${d} rewriters` ), (t = f[r])), (t.inUse = !0), [t.rewriter, () => (t.inUse = !1)] ); } }, 2015: function (e, t, r) { r.d(t, { i: () => s }); var n = r(37), i = r(1478); function s(e, t, r, s) { let o = "", a = "module" === t, l = (e) => { a ? (o += `import "${n.$W.files[e]}" `) : (o += `importScripts("${n.$W.files[e]}"); `); }; l("wasm"), l("all"), (o += `$scramjetLoadClient().loadAndHook(${JSON.stringify( n.$W )});`); let c = (0, i.o)(e, r, s, a); return ( c instanceof Uint8Array && (c = new TextDecoder().decode(c)), (o += c) ); } }, 6684: function (e, t, r) { r.d(t, { Sn: () => d, YH: () => c, Yq: () => p, hU: () => u, pL: () => h, rj: () => l, }); let n = { none: 0, "same-origin": 1, "same-site": 2, "cross-site": 3 }; async function i() { let e = indexedDB.open("$scramjet", 1); return new Promise((t, r) => { (e.onerror = () => r(e.error)), (e.onsuccess = () => t(e.result)); }); } async function s(e) { let t = (await i()) .transaction("redirectTrackers", "readonly") .objectStore("redirectTrackers"); return new Promise((r) => { let n = t.get(e); (n.onsuccess = () => r(n.result || null)), (n.onerror = () => r(null)); }); } async function o(e, t) { let r = (await i()) .transaction("redirectTrackers", "readwrite") .objectStore("redirectTrackers"); return new Promise((n, i) => { let s = r.put(t, e); (s.onsuccess = () => n()), (s.onerror = () => i(s.error)); }); } async function a(e) { let t = (await i()) .transaction("redirectTrackers", "readwrite") .objectStore("redirectTrackers"); return new Promise((r, n) => { let i = t.delete(e); (i.onsuccess = () => r()), (i.onerror = () => n(i.error)); }); } async function l(e, t, r) { (await s(e)) || (await o(e, { originalReferrer: t || "", mostRestrictiveSite: r, referrerPolicy: "", chainStarted: Date.now(), })); } async function c(e, t, r) { let n = await s(e); n && (await a(e), r && (n.referrerPolicy = r), await o(t, n)); } async function u(e, t) { let r = await s(e); if (!r) return t; let i = n[r.mostRestrictiveSite]; return (n[t] ?? 0) > i ? ((r.mostRestrictiveSite = t), await o(e, r), t) : r.mostRestrictiveSite; } async function d(e) { await a(e); } async function h(e, t, r) { let n = (await i()) .transaction("referrerPolicies", "readwrite") .objectStore("referrerPolicies"), s = { policy: t, referrer: r }; return new Promise((t, r) => { let i = n.put(s, e); (i.onsuccess = () => t()), (i.onerror = () => r(i.error)); }); } async function p(e) { let t = (await i()) .transaction("referrerPolicies", "readonly") .objectStore("referrerPolicies"); return new Promise((r) => { let n = t.get(e); (n.onsuccess = () => r(n.result || null)), (n.onerror = () => r(null)); }); } }, 8228: function (e, t, r) { r.d(t, { ps: () => a }); let n = "publicSuffixList"; async function i() { let e = indexedDB.open("$scramjet", 1); return new Promise((t, r) => { (e.onerror = () => r(e.error)), (e.onsuccess = () => t(e.result)); }); } async function s() { let e = (await i()).transaction(n, "readonly").objectStore(n); return new Promise((t) => { let r = e.get(n); (r.onsuccess = () => t(r.result || null)), (r.onerror = () => t(null)); }); } async function o(e) { let t = (await i()) .transaction("publicSuffixList", "readwrite") .objectStore("publicSuffixList"); return new Promise((r, i) => { let s = t.put({ data: e, expiry: Date.now() + 36e5 }, n); (s.onsuccess = () => r()), (s.onerror = () => i(s.error)); }); } async function a(e, t, r) { return t ? e.origin.origin === t.origin ? "same-origin" : (await l(e.origin, t, r)) ? "same-site" : "cross-site" : "none"; } async function l(e, t, r) { return (await c(e, r)) === (await c(t, r)); } async function c(e, t) { let r = await u(t), n = e.hostname.toLowerCase().split("."), i = "", s = !1; for (let e of r) { let t = e.startsWith("!") ? e.substring(1) : e; if ( (function (e, t) { if (e.length < t.length) return !1; let r = e.length - t.length; for (let n = 0; n < t.length; n++) { let i = e[r + n], s = t[n]; if ("*" !== s && i !== s) return !1; } return !0; })(n, t.split(".")) ) { if (e.startsWith("!")) { (i = t), (s = !0); break; } !s && t.length > i.length && (i = t); } } if (!i) return n.slice(-2).join("."); let o = i.split(".").length, a = s ? o : o + 1; return n.slice(-a).join("."); } async function u(e) { let t, r = await s(); if (r && Date.now() < r.expiry) return r.data; try { t = await e.fetch( "https://publicsuffix.org/list/public_suffix_list.dat" ); } catch (e) { throw Error(`Failed to fetch public suffix list: ${e}`); } let n = (await t.text()) .split("\n") .map((e) => { let t = e.trim(), r = t.indexOf(" "); return r > -1 ? t.substring(0, r) : t; }) .filter((e) => e && !e.startsWith("//")); return await o(n), n; } }, 2794: function (e, t, r) { r.d(t, { pX: () => n, zr: () => i }); let n = Symbol.for("scramjet client global"), i = Symbol.for("scramjet frame handle"); }, 5956: function (e, t, r) { function n(e, t) { let r = ` errorTrace.value = ${JSON.stringify(e)}; fetchedURL.textContent = ${JSON.stringify(t)}; for (const node of document.querySelectorAll("#hostname")) node.textContent = ${JSON.stringify( location.hostname )}; reload.addEventListener("click", () => location.reload()); version.textContent = ${JSON.stringify( $scramjetVersion.version )}; build.textContent = ${JSON.stringify($scramjetVersion.build)}; document.getElementById('copy-button').addEventListener('click', async () => { const text = document.getElementById('errorTrace').value; await navigator.clipboard.writeText(text); const btn = document.getElementById('copy-button'); btn.textContent = 'Copied!'; setTimeout(() => btn.textContent = 'Copy', 2000); }); `; return ` Scramjet

Uh oh!

There was an error loading

Try:

  • Checking your internet connection
  • Verifying you entered the correct address
  • Clearing the site data
  • Contacting 's owner
  • Verify the Wisp server isn't censored
  • Changing your Wisp server in the settings

If you're the owner of , try:

  • Restarting your server
  • Updating Scramjet
  • Troubleshooting the error on the GitHub repository

Scramjet v (build )

`; } function i(e, t) { let r = { "content-type": "text/html" }; return ( crossOriginIsolated && (r["Cross-Origin-Embedder-Policy"] = "require-corp"), new Response(n(String(e), t), { status: 500, headers: r }) ); } r.d(t, { B: () => n, v: () => i }); }, 1403: function (e, t, r) { r.d(t, { H: () => n }); class n { handle; origin; syncToken = 0; promises = {}; messageChannel = new MessageChannel(); connected = !1; constructor(e, t) { (this.handle = e), (this.origin = t), this.messageChannel.port1.addEventListener("message", (e) => { "scramjet$type" in e.data && ("init" === e.data.scramjet$type ? (this.connected = !0) : this.handleMessage(e.data)); }), this.messageChannel.port1.start(), this.handle.postMessage( { scramjet$type: "init", scramjet$port: this.messageChannel.port2, }, [this.messageChannel.port2] ); } handleMessage(e) { let t = this.promises[e.scramjet$token]; t && (t(e), delete this.promises[e.scramjet$token]); } async fetch(e) { let t = this.syncToken++, r = { scramjet$type: "fetch", scramjet$token: t, scramjet$request: { url: e.url, body: e.body, headers: Array.from(e.headers.entries()), method: e.method, mode: e.mode, destinitation: e.destination, }, }, n = e.body ? [e.body] : []; this.handle.postMessage(r, n); let { scramjet$response: i } = await new Promise((e) => { this.promises[t] = e; }); return ( !!i && new Response(i.body, { headers: i.headers, status: i.status, statusText: i.statusText, }) ); } } }, 5790: function (e, t, r) { r.d(t, { Pf: () => m, V3: () => S, dT: () => w }); var n = r(5956), i = r(8228), s = r(6684), o = r(1472), a = r(1478), l = r(1427), c = r(37), u = r(4435), d = r(884), h = r(2614), p = r(2015), f = r(8665).A; function g(e) { return e.status >= 300 && e.status < 400; } async function m(e, t) { try { let r, n, a = new URL(e.url); if (a.pathname === this.config.files.wasm) return fetch(this.config.files.wasm).then(async (e) => { let t = await e.arrayBuffer(), r = btoa( new Uint8Array(t) .reduce((e, t) => (e.push(String.fromCharCode(t)), e), []) .join("") ), n = ""; return ( (n += `if ('document' in self && document.currentScript) { document.currentScript.remove(); } self.WASM = '${r}';`), new Response(n, { headers: { "content-type": "text/javascript" }, }) ); }); let u = "", d = {}; for (let [e, t] of [...a.searchParams.entries()]) { switch (e) { case "type": u = t; break; case "dest": break; case "topFrame": r = t; break; case "parentFrame": n = t; break; default: f.warn( `${a.href} extraneous query parameter ${e}. Assuming
element` ), (d[e] = t); } a.searchParams.delete(e); } let h = new URL((0, o.v2)(a)); for (let [e, t] of Object.entries(d)) h.searchParams.set(e, t); let p = { origin: h, base: h, topFrameName: r, parentFrameName: n }; if ( a.pathname.startsWith(`${this.config.prefix}blob:`) || a.pathname.startsWith(`${this.config.prefix}data:`) ) { let t, r = a.pathname.substring(this.config.prefix.length); r.startsWith("blob:") && (r = (0, o.$n)(r)); let n = await fetch(r, {}); (n.finalURL = r.startsWith("blob:") ? r : "(data url)"), n.body && (t = await b(n, p, e.destination, u, this.cookieStore)); let i = Object.fromEntries(n.headers.entries()); return ( crossOriginIsolated && ((i["Cross-Origin-Opener-Policy"] = "same-origin"), (i["Cross-Origin-Embedder-Policy"] = "require-corp")), new Response(t, { status: n.status, statusText: n.statusText, headers: i, }) ); } let g = this.serviceWorkers.find((e) => e.origin === h.origin); if (g?.connected && "swruntime" !== a.searchParams.get("from")) { let t = await g.fetch(e); if (t) return t; } if (h.origin === new URL(e.url).origin) throw Error( "attempted to fetch from same origin - this means the site has obtained a reference to the real origin, aborting" ); let m = new l.u(); for (let [t, r] of e.headers.entries()) m.set(t, r); if (t && new URL(t.url).pathname.startsWith(c.$W.prefix)) { let e = new URL((0, o.v2)(t.url)); e.toString().includes("youtube.com") || (m.set("Referer", e.href), m.set("Origin", e.origin)); } let w = this.cookieStore.getCookies(h, !1); w.length && m.set("Cookie", w); let x = !1; if ( "iframe" === e.destination && "navigate" === e.mode && e.referrer && "no-referrer" !== e.referrer ) { let t = e.referrer, r = await self.clients.matchAll({ type: "window" }); for (; t; ) { if (!t.includes(c.$W.prefix)) { x = !0; break; } let e = r.find((e) => e.url === t), n = await (0, s.Yq)(t); if (!n || !n.referrer) { e && t.startsWith(location.origin) && (x = !0); break; } if (e && "nested" === e.frameType) t = n.referrer; else break; } } x ? (m.set("Sec-Fetch-Dest", "document"), m.set("Sec-Fetch-Mode", "navigate")) : (m.set("Sec-Fetch-Dest", e.destination || "empty"), m.set("Sec-Fetch-Mode", e.mode)); let v = "none"; if ( e.referrer && "" !== e.referrer && "no-referrer" !== e.referrer && e.referrer.includes(c.$W.prefix) ) { let t = (0, o.v2)(e.referrer); if (t) { let e = new URL(t); v = await (0, i.ps)(p, e, this.client); } } await (0, s.rj)( h.toString(), e.referrer ? (0, o.v2)(e.referrer) : null, v ), m.set("Sec-Fetch-Site", await (0, s.hU)(h.toString(), v)); let E = new S(h, m.headers, e.body, e.method, e.destination, t); this.dispatchEvent(E); let T = (await E.response) || (await this.client.fetch(E.url, { method: E.method, body: E.body, headers: E.requestHeaders, credentials: "omit", mode: "cors" === e.mode ? e.mode : "same-origin", cache: e.cache, redirect: "manual", duplex: "half", })); return ( (T.finalURL = E.url.href), await y( h, p, u, e.destination, e.mode, T, this.cookieStore, t, this.client, this, e.referrer ) ); } catch (i) { let t = { message: i.message, url: e.url, destination: e.destination, }; if ( (i.stack && (t.stack = i.stack), console.error("ERROR FROM SERVICE WORKER FETCH: ", t), console.error(i), !["document", "iframe"].includes(e.destination)) ) return new Response(void 0, { status: 500 }); let r = Object.entries(t) .map( ([e, t]) => `${e.charAt(0).toUpperCase() + e.slice(1)}: ${t}` ) .join("\n\n"); return (0, n.v)(r, (0, o.v2)(e.url)); } } async function y(e, t, r, n, a, l, d, h, p, f, m) { let y, S = "navigate" === a && ["document", "iframe"].includes(n), x = await (0, u.l)(l.rawHeaders, t, p, { get: s.Yq, set: s.pL }); if ( (S && x["referrer-policy"] && m && (await (0, s.pL)(e.href, x["referrer-policy"], m)), g(l)) ) { let t = new URL((0, o.v2)(x.location)); await (0, s.YH)(e.toString(), t.toString(), x["referrer-policy"]); let n = await (0, i.ps)({ origin: t, base: t }, e, p); if ((await (0, s.hU)(t.toString(), n), r)) { let e = new URL(x.location); e.searchParams.set("type", r), (x.location = e.href); } } let v = x["set-cookie"] || []; for (let t in v) if (h) { let r = f.dispatch(h, { scramjet$type: "cookie", cookie: t, url: e.href, }); "document" !== n && "iframe" !== n && (await r); } for (let t in (await d.setCookies(v instanceof Array ? v : [v], e), x)) Array.isArray(x[t]) && (x[t] = x[t][0]); if ( (function (e, t) { if (["document", "iframe"].includes(t)) { let t = e["content-disposition"]; if (t) { if ("inline" !== t) return !0; } else { let t = e["content-type"]?.split(";")[0].trim().toLowerCase(); if ( t && ![ "text/html", "text/plain", "text/css", "text/javascript", "text/xml", "application/javascript", "application/json", "application/xml", "application/pdf", ].includes(t) && !t.startsWith("text") && !t.startsWith("image") && !t.startsWith("font") && !t.startsWith("video") ) return !0; } } return !1; })(x, n) && !g(l) ) if ((0, c.U5)("interceptDownloads", e)) { if (!h) throw Error("cant find client"); let t = null, r = x["content-disposition"]; if ("string" == typeof r) { let e = r.match(/filename=["']?([^"';\n]*)["']?/i); e && e[1] && (t = e[1]); } let n = x["content-length"], i = await clients.matchAll({ type: "window" }); if ( (i = i.filter((e) => !e.url.includes(c.$W.prefix))).length < 1 ) throw Error( "couldn't find a controller client to dispatch download to" ); let s = { filename: t, url: e.href, type: x["content-type"], body: l.body, length: Number(n), }; i[0].postMessage({ scramjet$type: "download", download: s }, [ l.body, ]), await new Promise(() => {}); } else { let e = x["content-disposition"]; if (!/\s*?((inline|attachment);\s*?)filename=/i.test(e)) { let t = /^\s*?attachment/i.test(e) ? "attachment" : "inline", [r] = new URL(l.finalURL).pathname.split("/").slice(-1); x["content-disposition"] = `${t}; filename=${JSON.stringify( r )}`; } } l.body && !g(l) && (y = await b(l, t, n, r, d)), "text/event-stream" === x.accept && (x["content-type"] = "text/event-stream"), delete x["permissions-policy"], crossOriginIsolated && [ "document", "iframe", "worker", "sharedworker", "style", "script", ].includes(n) && ((x["Cross-Origin-Embedder-Policy"] = "require-corp"), (x["Cross-Origin-Opener-Policy"] = "same-origin")); let E = new w(y, x, l.status, l.statusText, n, e, l, h); return ( f.dispatchEvent(E), g(l) || (await (0, s.Sn)(e.toString())), new Response(E.responseBody, { headers: E.responseHeaders, status: E.status, statusText: E.statusText, }) ); } async function b(e, t, r, n, i) { switch (r) { case "iframe": case "document": if (e.headers.get("content-type")?.startsWith("text/html")) return (0, d.Qs)(await e.text(), i, t, !0); return e.body; case "script": return (0, a.o)( new Uint8Array(await e.arrayBuffer()), e.finalURL, t, "module" === n ); case "style": return (0, h.s)(await e.text(), t); case "sharedworker": case "worker": return (0, p.i)( new Uint8Array(await e.arrayBuffer()), n, e.finalURL, t ); default: return e.body; } } class w extends Event { responseBody; responseHeaders; status; statusText; destination; url; rawResponse; client; constructor(e, t, r, n, i, s, o, a) { super("handleResponse"), (this.responseBody = e), (this.responseHeaders = t), (this.status = r), (this.statusText = n), (this.destination = i), (this.url = s), (this.rawResponse = o), (this.client = a); } } class S extends Event { url; requestHeaders; body; method; destination; client; constructor(e, t, r, n, i, s) { super("request"), (this.url = e), (this.requestHeaders = t), (this.body = r), (this.method = n), (this.destination = i), (this.client = s); } response; } }, 7510: function (e, t, r) { r.r(t), r.d(t, { FakeServiceWorker: () => n.H, ScramjetHandleResponseEvent: () => i.dT, ScramjetRequestEvent: () => i.V3, ScramjetServiceWorker: () => u, errorTemplate: () => c.B, handleFetch: () => i.Pf, renderError: () => c.v, }); var n = r(1403), i = r(5790), s = r(236), o = r(1561), a = r(3831), l = r(37), c = r(5956); class u extends EventTarget { client; config; syncPool = {}; synctoken = 0; cookieStore = new a.k(); serviceWorkers = []; constructor() { super(), (this.client = new s.Ay()); let e = indexedDB.open("$scramjet", 1); (e.onsuccess = () => { let t = e.result .transaction("cookies", "readonly") .objectStore("cookies") .get("cookies"); t.onsuccess = () => { t.result && this.cookieStore.load(t.result); }; }), addEventListener("message", async ({ data: t }) => { if ("scramjet$type" in t) { if ("scramjet$token" in t) { let e = this.syncPool[t.scramjet$token]; delete this.syncPool[t.scramjet$token], e(t); return; } if ("registerServiceWorker" === t.scramjet$type) return void this.serviceWorkers.push( new n.H(t.port, t.origin) ); "cookie" === t.scramjet$type && (this.cookieStore.setCookies([t.cookie], new URL(t.url)), e.result .transaction("cookies", "readwrite") .objectStore("cookies") .put(JSON.parse(this.cookieStore.dump()), "cookies")), "loadConfig" === t.scramjet$type && (this.config = t.config); } }); } async dispatch(e, t) { let r, n = this.synctoken++, i = new Promise((e) => (r = e)); return ( (this.syncPool[n] = r), (t.scramjet$token = n), e.postMessage(t), await i ); } async loadConfig() { if (this.config) return; let e = indexedDB.open("$scramjet", 1); return new Promise((t, r) => { (e.onsuccess = async () => { let n = e.result .transaction("config", "readonly") .objectStore("config") .get("config"); (n.onsuccess = async () => { (this.config = n.result), (0, l.Nk)(n.result), await (0, o.n$)(), t(); }), (n.onerror = () => r(n.error)); }), (e.onerror = () => r(e.error)); }); } route({ request: e }) { return ( !!e.url.startsWith(location.origin + this.config.prefix) || !!e.url.startsWith(location.origin + this.config.files.wasm) ); } async fetch({ request: e, clientId: t }) { this.config || (await this.loadConfig()); let r = await self.clients.get(t); return i.Pf.call(this, e, r); } } }, 236: function (e, t, r) { r.d(t, { Ay: () => S, DD: () => w }); let n = globalThis.fetch, i = globalThis.SharedWorker, s = globalThis.localStorage, o = globalThis.navigator.serviceWorker, a = MessagePort.prototype.postMessage, l = { prototype: { send: WebSocket.prototype.send }, CLOSED: WebSocket.CLOSED, CLOSING: WebSocket.CLOSING, CONNECTING: WebSocket.CONNECTING, OPEN: WebSocket.OPEN, }; async function c() { let e = Promise.race([ Promise.any( ( await self.clients.matchAll({ type: "window", includeUncontrolled: !0, }) ).map(async (e) => { let t, r = await ((t = new MessageChannel()), new Promise((r) => { e.postMessage({ type: "getPort", port: t.port2 }, [ t.port2, ]), (t.port1.onmessage = (e) => { r(e.data); }); })); return await u(r), r; }) ), new Promise((e, t) => setTimeout(t, 1e3, TypeError("timeout"))), ]); try { return await e; } catch (e) { if (e instanceof AggregateError) throw ( (console.error( "bare-mux: failed to get a bare-mux SharedWorker MessagePort as all clients returned an invalid MessagePort." ), Error("All clients returned an invalid MessagePort.")) ); return ( console.warn( "bare-mux: failed to get a bare-mux SharedWorker MessagePort within 1s, retrying" ), await c() ); } } function u(e) { let t = new MessageChannel(), r = new Promise((e, r) => { (t.port1.onmessage = (t) => { "pong" === t.data.type && e(); }), setTimeout(r, 1500); }); return ( a.call(e, { message: { type: "ping" }, port: t.port2 }, [t.port2]), r ); } function d(e, t) { let r = new i(e, "bare-mux-worker"); return ( t && o.addEventListener("message", (t) => { if ("getPort" === t.data.type && t.data.port) { console.debug("bare-mux: recieved request for port from sw"); let r = new i(e, "bare-mux-worker"); a.call(t.data.port, r.port, [r.port]); } }), r.port ); } let h = null; class p { constructor(e) { (this.channel = new BroadcastChannel("bare-mux")), e instanceof MessagePort || e instanceof Promise ? (this.port = e) : this.createChannel(e, !0); } createChannel(e, t) { if (self.clients) (this.port = c()), (this.channel.onmessage = (e) => { "refreshPort" === e.data.type && (this.port = c()); }); else if (e && SharedWorker) { if (!e.startsWith("/") && !e.includes("://")) throw Error( "Invalid URL. Must be absolute or start at the root." ); (this.port = d(e, t)), console.debug( "bare-mux: setting localStorage bare-mux-path to", e ), (s["bare-mux-path"] = e); } else { if (!SharedWorker) throw Error("Unable to get a channel to the SharedWorker."); { let e = s["bare-mux-path"]; if ( (console.debug( "bare-mux: got localStorage bare-mux-path:", e ), !e) ) throw Error( "Unable to get bare-mux workerPath from localStorage." ); this.port = d(e, t); } } } async sendMessage(e, t) { this.port instanceof Promise && (this.port = await this.port); try { await u(this.port); } catch { return ( console.warn( "bare-mux: Failed to get a ping response from the worker within 1.5s. Assuming port is dead." ), this.createChannel(), await this.sendMessage(e, t) ); } let r = new MessageChannel(), n = [r.port2, ...(t || [])], i = new Promise((e, t) => { r.port1.onmessage = (r) => { let n = r.data; "error" === n.type ? t(n.error) : e(n); }; }); return a.call(this.port, { message: e, port: r.port2 }, n), await i; } } class f extends EventTarget { constructor(e, t = [], r, n) { super(), (this.protocols = t), (this.readyState = l.CONNECTING), (this.url = e.toString()), (this.protocols = t); let i = (e) => { (this.protocols = e), (this.readyState = l.OPEN); let t = new Event("open"); this.dispatchEvent(t); }, s = async (e) => { let t = new MessageEvent("message", { data: e }); this.dispatchEvent(t); }, o = (e, t) => { this.readyState = l.CLOSED; let r = new CloseEvent("close", { code: e, reason: t }); this.dispatchEvent(r); }, a = () => { this.readyState = l.CLOSED; let e = new Event("error"); this.dispatchEvent(e); }; (this.channel = new MessageChannel()), (this.channel.port1.onmessage = (e) => { "open" === e.data.type ? i(e.data.args[0]) : "message" === e.data.type ? s(e.data.args[0]) : "close" === e.data.type ? o(e.data.args[0], e.data.args[1]) : "error" === e.data.type && a(); }), r.sendMessage( { type: "websocket", websocket: { url: e.toString(), protocols: t, requestHeaders: n, channel: this.channel.port2, }, }, [this.channel.port2] ); } send(...e) { if (this.readyState === l.CONNECTING) throw new DOMException( "Failed to execute 'send' on 'WebSocket': Still in CONNECTING state." ); let t = e[0]; t.buffer && (t = t.buffer.slice(t.byteOffset, t.byteOffset + t.byteLength)), a.call( this.channel.port1, { type: "data", data: t }, t instanceof ArrayBuffer ? [t] : [] ); } close(e, t) { a.call(this.channel.port1, { type: "close", closeCode: e, closeReason: t, }); } } function g(e, t, r) { console.error(`error while processing '${r}': `, t), e.postMessage({ type: "error", error: t }); } let m = ["ws:", "wss:"], y = [101, 204, 205, 304], b = [301, 302, 303, 307, 308]; class w { constructor(e) { this.worker = new p(e); } async getTransport() { return (await this.worker.sendMessage({ type: "get" })).name; } async setTransport(e, t, r) { await this.setManualTransport( ` const { default: BareTransport } = await import("${e}"); return [BareTransport, "${e}"]; `, t, r ); } async setManualTransport(e, t, r) { if ("bare-mux-remote" === e) throw Error("Use setRemoteTransport."); await this.worker.sendMessage( { type: "set", client: { function: e, args: t } }, r ); } async setRemoteTransport(e, t) { let r = new MessageChannel(); (r.port1.onmessage = async (t) => { let r = t.data.port, n = t.data.message; if ("fetch" === n.type) try { e.ready || (await e.init()), await (async function (e, t, r) { let n = await r.request( new URL(e.fetch.remote), e.fetch.method, e.fetch.body, e.fetch.headers, null ); if ( !(function () { if (null === h) { let e, t = new MessageChannel(), r = new ReadableStream(); try { a.call(t.port1, r, [r]), (e = !0); } catch (t) { e = !1; } return (h = e), e; } return h; })() && n.body instanceof ReadableStream ) { let e = new Response(n.body); n.body = await e.arrayBuffer(); } n.body instanceof ReadableStream || n.body instanceof ArrayBuffer ? a.call(t, { type: "fetch", fetch: n }, [n.body]) : a.call(t, { type: "fetch", fetch: n }); })(n, r, e); } catch (e) { g(r, e, "fetch"); } else if ("websocket" === n.type) try { e.ready || (await e.init()), await (async function (e, t, r) { let [n, i] = r.connect( new URL(e.websocket.url), e.websocket.protocols, e.websocket.requestHeaders, (t) => { a.call(e.websocket.channel, { type: "open", args: [t], }); }, (t) => { t instanceof ArrayBuffer ? a.call( e.websocket.channel, { type: "message", args: [t] }, [t] ) : a.call(e.websocket.channel, { type: "message", args: [t], }); }, (t, r) => { a.call(e.websocket.channel, { type: "close", args: [t, r], }); }, (t) => { a.call(e.websocket.channel, { type: "error", args: [t], }); } ); (e.websocket.channel.onmessage = (e) => { "data" === e.data.type ? n(e.data.data) : "close" === e.data.type && i(e.data.closeCode, e.data.closeReason); }), a.call(t, { type: "websocket" }); })(n, r, e); } catch (e) { g(r, e, "websocket"); } }), await this.worker.sendMessage( { type: "set", client: { function: "bare-mux-remote", args: [r.port2, t] }, }, [r.port2] ); } getInnerPort() { return this.worker.port; } } class S { constructor(e) { this.worker = new p(e); } createWebSocket(e, t = [], r, n) { try { e = new URL(e); } catch (t) { throw new DOMException( `Faiiled to construct 'WebSocket': The URL '${e}' is invalid.` ); } if (!m.includes(e.protocol)) throw new DOMException( `Failed to construct 'WebSocket': The URL's scheme must be either 'ws' or 'wss'. '${e.protocol}' is not allowed.` ); for (let e of (Array.isArray(t) || (t = [t]), (t = t.map(String)))) if ( !(function (e) { for (let t = 0; t < e.length; t++) { let r = e[t]; if ( !"!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~".includes( r ) ) return !1; } return !0; })(e) ) throw new DOMException( `Failed to construct 'WebSocket': The subprotocol '${e}' is invalid.` ); return (n = n || {}), new f(e, t, this.worker, n); } async fetch(e, t) { let r = new Request(e, t), i = t?.headers || r.headers, s = i instanceof Headers ? Object.fromEntries(i) : i, o = r.body, a = new URL(r.url); if (a.protocol.startsWith("blob:")) { let e = await n(a), t = new Response(e.body, e); return (t.rawHeaders = Object.fromEntries(e.headers)), t; } for (let e = 0; ; e++) { let n = ( await this.worker.sendMessage( { type: "fetch", fetch: { remote: a.toString(), method: r.method, headers: s, body: o || void 0, }, }, o ? [o] : [] ) ).fetch, i = new Response(y.includes(n.status) ? void 0 : n.body, { headers: new Headers(n.headers), status: n.status, statusText: n.statusText, }); (i.rawHeaders = n.headers), (i.rawResponse = n), (i.finalURL = a.toString()); let l = t?.redirect || r.redirect; if (!b.includes(i.status)) return i; switch (l) { case "follow": { let t = i.headers.get("location"); if (20 > e && null !== t) { a = new URL(t, a); continue; } throw TypeError("Failed to fetch"); } case "error": throw TypeError("Failed to fetch"); case "manual": return i; } } } } console.debug("bare-mux: running v2.1.7 (build c56d286)"); }, 8832: function (e, t, r) { r.d(t, { H: () => n, L: () => i }); let n = new Map( [ "altGlyph", "altGlyphDef", "altGlyphItem", "animateColor", "animateMotion", "animateTransform", "clipPath", "feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feDropShadow", "feFlood", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence", "foreignObject", "glyphRef", "linearGradient", "radialGradient", "textPath", ].map((e) => [e.toLowerCase(), e]) ), i = new Map( [ "definitionURL", "attributeName", "attributeType", "baseFrequency", "baseProfile", "calcMode", "clipPathUnits", "diffuseConstant", "edgeMode", "filterUnits", "glyphRef", "gradientTransform", "gradientUnits", "kernelMatrix", "kernelUnitLength", "keyPoints", "keySplines", "keyTimes", "lengthAdjust", "limitingConeAngle", "markerHeight", "markerUnits", "markerWidth", "maskContentUnits", "maskUnits", "numOctaves", "pathLength", "patternContentUnits", "patternTransform", "patternUnits", "pointsAtX", "pointsAtY", "pointsAtZ", "preserveAlpha", "preserveAspectRatio", "primitiveUnits", "refX", "refY", "repeatCount", "repeatDur", "requiredExtensions", "requiredFeatures", "specularConstant", "specularExponent", "spreadMethod", "startOffset", "stdDeviation", "stitchTiles", "surfaceScale", "systemLanguage", "tableValues", "targetX", "targetY", "textLength", "viewBox", "viewTarget", "xChannelSelector", "yChannelSelector", "zoomAndPan", ].map((e) => [e.toLowerCase(), e]) ); }, 6498: function (e, t, r) { r.d(t, { A: () => c }); var n = r(2743), i = r(8466), s = r(8832); let o = new Set([ "style", "script", "xmp", "iframe", "noembed", "noframes", "plaintext", "noscript", ]); function a(e) { return e.replace(/"/g, """); } let l = new Set([ "area", "base", "basefont", "br", "col", "command", "embed", "frame", "hr", "img", "input", "isindex", "keygen", "link", "meta", "param", "source", "track", "wbr", ]), c = function e(t, r = {}) { let c = "length" in t ? t : [t], h = ""; for (let t = 0; t < c.length; t++) h += (function (t, r) { var c, h, p; switch (t.type) { case n.bL: return e(t.children, r); case n.fl: case n.WL: return (c = t), `<${c.data}>`; case n.Mw: return (h = t), ``; case n.KB: return (p = t), ``; case n.eF: case n.OF: case n.vw: return (function (t, r) { var n; "foreign" === r.xmlMode && ((t.name = null != (n = s.H.get(t.name)) ? n : t.name), t.parent && u.has(t.parent.name) && (r = { ...r, xmlMode: !1 })), !r.xmlMode && d.has(t.name) && (r = { ...r, xmlMode: "foreign" }); let o = `<${t.name}`, c = (function (e, t) { var r; if (!e) return; let n = (null != (r = t.encodeEntities) ? r : t.decodeEntities) === !1 ? a : t.xmlMode || "utf8" !== t.encodeEntities ? i.WY : i.Gj; return Object.keys(e) .map((r) => { var i, o; let a = null != (i = e[r]) ? i : ""; return ("foreign" === t.xmlMode && (r = null != (o = s.L.get(r)) ? o : r), t.emptyAttrs || t.xmlMode || "" !== a) ? `${r}="${n(a)}"` : r; }) .join(" "); })(t.attribs, r); return ( c && (o += ` ${c}`), 0 === t.children.length && (r.xmlMode ? !1 !== r.selfClosingTags : r.selfClosingTags && l.has(t.name)) ? (r.xmlMode || (o += " "), (o += "/>")) : ((o += ">"), t.children.length > 0 && (o += e(t.children, r)), (r.xmlMode || !l.has(t.name)) && (o += ``)), o ); })(t, r); case n.EY: return (function (e, t) { var r; let n = e.data || ""; return ( (null != (r = t.encodeEntities) ? r : t.decodeEntities) === !1 || (!t.xmlMode && e.parent && o.has(e.parent.name)) || (n = t.xmlMode || "utf8" !== t.encodeEntities ? (0, i.WY)(n) : (0, i.X1)(n)), n ); })(t, r); } })(c[t], r); return h; }, u = new Set([ "mi", "mo", "mn", "ms", "mtext", "annotation-xml", "foreignObject", "desc", "title", ]), d = new Set(["svg", "math"]); }, 2743: function (e, t, r) { var n, i; function s(e) { return e.type === n.Tag || e.type === n.Script || e.type === n.Style; } r.d(t, { EY: () => a, KB: () => p, Mw: () => c, OF: () => d, RJ: () => n, WL: () => l, bL: () => o, dz: () => s, eF: () => u, fl: () => f, vw: () => h, }), ((i = n || (n = {})).Root = "root"), (i.Text = "text"), (i.Directive = "directive"), (i.Comment = "comment"), (i.Script = "script"), (i.Style = "style"), (i.Tag = "tag"), (i.CDATA = "cdata"), (i.Doctype = "doctype"); let o = n.Root, a = n.Text, l = n.Directive, c = n.Comment, u = n.Script, d = n.Style, h = n.Tag, p = n.CDATA, f = n.Doctype; }, 8866: function (e, t, r) { r.d(t, { DV: () => o, Hg: () => i.Hg, Mw: () => i.Mw }); var n = r(2743), i = r(6072); let s = { withStartIndices: !1, withEndIndices: !1, xmlMode: !1 }; class o { constructor(e, t, r) { (this.dom = []), (this.root = new i.yo(this.dom)), (this.done = !1), (this.tagStack = [this.root]), (this.lastNode = null), (this.parser = null), "function" == typeof t && ((r = t), (t = s)), "object" == typeof e && ((t = e), (e = void 0)), (this.callback = null != e ? e : null), (this.options = null != t ? t : s), (this.elementCB = null != r ? r : null); } onparserinit(e) { this.parser = e; } onreset() { (this.dom = []), (this.root = new i.yo(this.dom)), (this.done = !1), (this.tagStack = [this.root]), (this.lastNode = null), (this.parser = null); } onend() { this.done || ((this.done = !0), (this.parser = null), this.handleCallback(null)); } onerror(e) { this.handleCallback(e); } onclosetag() { this.lastNode = null; let e = this.tagStack.pop(); this.options.withEndIndices && (e.endIndex = this.parser.endIndex), this.elementCB && this.elementCB(e); } onopentag(e, t) { let r = this.options.xmlMode ? n.RJ.Tag : void 0, s = new i.Hg(e, t, void 0, r); this.addNode(s), this.tagStack.push(s); } ontext(e) { let { lastNode: t } = this; if (t && t.type === n.RJ.Text) (t.data += e), this.options.withEndIndices && (t.endIndex = this.parser.endIndex); else { let t = new i.EY(e); this.addNode(t), (this.lastNode = t); } } oncomment(e) { if (this.lastNode && this.lastNode.type === n.RJ.Comment) { this.lastNode.data += e; return; } let t = new i.Mw(e); this.addNode(t), (this.lastNode = t); } oncommentend() { this.lastNode = null; } oncdatastart() { let e = new i.EY(""), t = new i.KB([e]); this.addNode(t), (e.parent = t), (this.lastNode = e); } oncdataend() { this.lastNode = null; } onprocessinginstruction(e, t) { let r = new i.Cd(e, t); this.addNode(r); } handleCallback(e) { if ("function" == typeof this.callback) this.callback(e, this.dom); else if (e) throw e; } addNode(e) { let t = this.tagStack[this.tagStack.length - 1], r = t.children[t.children.length - 1]; this.options.withStartIndices && (e.startIndex = this.parser.startIndex), this.options.withEndIndices && (e.endIndex = this.parser.endIndex), t.children.push(e), r && ((e.prev = r), (r.next = e)), (e.parent = t), (this.lastNode = null); } } }, 6072: function (e, t, r) { r.d(t, { Cd: () => l, EY: () => o, Hg: () => h, KB: () => u, Mw: () => a, yo: () => d, }); var n = r(2743); class i { constructor() { (this.parent = null), (this.prev = null), (this.next = null), (this.startIndex = null), (this.endIndex = null); } get parentNode() { return this.parent; } set parentNode(e) { this.parent = e; } get previousSibling() { return this.prev; } set previousSibling(e) { this.prev = e; } get nextSibling() { return this.next; } set nextSibling(e) { this.next = e; } cloneNode(e = !1) { return p(this, e); } } class s extends i { constructor(e) { super(), (this.data = e); } get nodeValue() { return this.data; } set nodeValue(e) { this.data = e; } } class o extends s { constructor() { super(...arguments), (this.type = n.RJ.Text); } get nodeType() { return 3; } } class a extends s { constructor() { super(...arguments), (this.type = n.RJ.Comment); } get nodeType() { return 8; } } class l extends s { constructor(e, t) { super(t), (this.name = e), (this.type = n.RJ.Directive); } get nodeType() { return 1; } } class c extends i { constructor(e) { super(), (this.children = e); } get firstChild() { var e; return null != (e = this.children[0]) ? e : null; } get lastChild() { return this.children.length > 0 ? this.children[this.children.length - 1] : null; } get childNodes() { return this.children; } set childNodes(e) { this.children = e; } } class u extends c { constructor() { super(...arguments), (this.type = n.RJ.CDATA); } get nodeType() { return 4; } } class d extends c { constructor() { super(...arguments), (this.type = n.RJ.Root); } get nodeType() { return 9; } } class h extends c { constructor( e, t, r = [], i = "script" === e ? n.RJ.Script : "style" === e ? n.RJ.Style : n.RJ.Tag ) { super(r), (this.name = e), (this.attribs = t), (this.type = i); } get nodeType() { return 1; } get tagName() { return this.name; } set tagName(e) { this.name = e; } get attributes() { return Object.keys(this.attribs).map((e) => { var t, r; return { name: e, value: this.attribs[e], namespace: null == (t = this["x-attribsNamespace"]) ? void 0 : t[e], prefix: null == (r = this["x-attribsPrefix"]) ? void 0 : r[e], }; }); } } function p(e, t = !1) { let r; if (e.type === n.RJ.Text) r = new o(e.data); else if (e.type === n.RJ.Comment) r = new a(e.data); else if ((0, n.dz)(e)) { let n = t ? f(e.children) : [], i = new h(e.name, { ...e.attribs }, n); n.forEach((e) => (e.parent = i)), null != e.namespace && (i.namespace = e.namespace), e["x-attribsNamespace"] && (i["x-attribsNamespace"] = { ...e["x-attribsNamespace"] }), e["x-attribsPrefix"] && (i["x-attribsPrefix"] = { ...e["x-attribsPrefix"] }), (r = i); } else if (e.type === n.RJ.CDATA) { let n = t ? f(e.children) : [], i = new u(n); n.forEach((e) => (e.parent = i)), (r = i); } else if (e.type === n.RJ.Root) { let n = t ? f(e.children) : [], i = new d(n); n.forEach((e) => (e.parent = i)), e["x-mode"] && (i["x-mode"] = e["x-mode"]), (r = i); } else if (e.type === n.RJ.Directive) { let t = new l(e.name, e.data); null != e["x-name"] && ((t["x-name"] = e["x-name"]), (t["x-publicId"] = e["x-publicId"]), (t["x-systemId"] = e["x-systemId"])), (r = t); } else throw Error(`Not implemented yet: ${e.type}`); return ( (r.startIndex = e.startIndex), (r.endIndex = e.endIndex), null != e.sourceCodeLocation && (r.sourceCodeLocation = e.sourceCodeLocation), r ); } function f(e) { let t = e.map((e) => p(e, !0)); for (let e = 1; e < t.length; e++) (t[e].prev = t[e - 1]), (t[e - 1].next = t[e]); return t; } }, 3256: function (e, t, r) { r(5016), r(1050); }, 6812: function (e, t, r) { var n, i; r(8866), ((i = n || (n = {}))[(i.DISCONNECTED = 1)] = "DISCONNECTED"), (i[(i.PRECEDING = 2)] = "PRECEDING"), (i[(i.FOLLOWING = 4)] = "FOLLOWING"), (i[(i.CONTAINS = 8)] = "CONTAINS"), (i[(i.CONTAINED_BY = 16)] = "CONTAINED_BY"); }, 4993: function (e, t, r) { r(5016), r(4647), r(9861), r(1050), r(6812), r(3256), r(8866); }, 1050: function (e, t, r) { r(8866), r(9861); }, 9861: function (e, t, r) { r(8866); }, 5016: function (e, t, r) { r(8866), r(6498), r(2743); }, 4647: function (e, t, r) { r(8866); }, 2146: function (e, t, r) { var n; r.d(t, { MK: () => s, y6: () => o }); let i = new Map([ [0, 65533], [128, 8364], [130, 8218], [131, 402], [132, 8222], [133, 8230], [134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249], [140, 338], [142, 381], [145, 8216], [146, 8217], [147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732], [153, 8482], [154, 353], [155, 8250], [156, 339], [158, 382], [159, 376], ]), s = null != (n = String.fromCodePoint) ? n : function (e) { let t = ""; return ( e > 65535 && ((e -= 65536), (t += String.fromCharCode(((e >>> 10) & 1023) | 55296)), (e = 56320 | (1023 & e))), (t += String.fromCharCode(e)) ); }; function o(e) { var t; return (e >= 55296 && e <= 57343) || e > 1114111 ? 65533 : null != (t = i.get(e)) ? t : e; } }, 2990: function (e, t, r) { r.d(t, { FJ: () => u, MK: () => p.MK, Wf: () => g, qN: () => d.q, sr: () => h.s, }); var n, i, s, o, a, l, c, u, d = r(7259), h = r(5949), p = r(2146); function f(e) { return e >= a.ZERO && e <= a.NINE; } ((n = a || (a = {}))[(n.NUM = 35)] = "NUM"), (n[(n.SEMI = 59)] = "SEMI"), (n[(n.EQUALS = 61)] = "EQUALS"), (n[(n.ZERO = 48)] = "ZERO"), (n[(n.NINE = 57)] = "NINE"), (n[(n.LOWER_A = 97)] = "LOWER_A"), (n[(n.LOWER_F = 102)] = "LOWER_F"), (n[(n.LOWER_X = 120)] = "LOWER_X"), (n[(n.LOWER_Z = 122)] = "LOWER_Z"), (n[(n.UPPER_A = 65)] = "UPPER_A"), (n[(n.UPPER_F = 70)] = "UPPER_F"), (n[(n.UPPER_Z = 90)] = "UPPER_Z"), ((i = l || (l = {}))[(i.VALUE_LENGTH = 49152)] = "VALUE_LENGTH"), (i[(i.BRANCH_LENGTH = 16256)] = "BRANCH_LENGTH"), (i[(i.JUMP_TABLE = 127)] = "JUMP_TABLE"), ((s = c || (c = {}))[(s.EntityStart = 0)] = "EntityStart"), (s[(s.NumericStart = 1)] = "NumericStart"), (s[(s.NumericDecimal = 2)] = "NumericDecimal"), (s[(s.NumericHex = 3)] = "NumericHex"), (s[(s.NamedEntity = 4)] = "NamedEntity"), ((o = u || (u = {}))[(o.Legacy = 0)] = "Legacy"), (o[(o.Strict = 1)] = "Strict"), (o[(o.Attribute = 2)] = "Attribute"); class g { constructor(e, t, r) { (this.decodeTree = e), (this.emitCodePoint = t), (this.errors = r), (this.state = c.EntityStart), (this.consumed = 1), (this.result = 0), (this.treeIndex = 0), (this.excess = 1), (this.decodeMode = u.Strict); } startEntity(e) { (this.decodeMode = e), (this.state = c.EntityStart), (this.result = 0), (this.treeIndex = 0), (this.excess = 1), (this.consumed = 1); } write(e, t) { switch (this.state) { case c.EntityStart: if (e.charCodeAt(t) === a.NUM) return ( (this.state = c.NumericStart), (this.consumed += 1), this.stateNumericStart(e, t + 1) ); return ( (this.state = c.NamedEntity), this.stateNamedEntity(e, t) ); case c.NumericStart: return this.stateNumericStart(e, t); case c.NumericDecimal: return this.stateNumericDecimal(e, t); case c.NumericHex: return this.stateNumericHex(e, t); case c.NamedEntity: return this.stateNamedEntity(e, t); } } stateNumericStart(e, t) { return t >= e.length ? -1 : (32 | e.charCodeAt(t)) === a.LOWER_X ? ((this.state = c.NumericHex), (this.consumed += 1), this.stateNumericHex(e, t + 1)) : ((this.state = c.NumericDecimal), this.stateNumericDecimal(e, t)); } addToNumericResult(e, t, r, n) { if (t !== r) { let i = r - t; (this.result = this.result * Math.pow(n, i) + Number.parseInt(e.substr(t, i), n)), (this.consumed += i); } } stateNumericHex(e, t) { let r = t; for (; t < e.length; ) { var n; let i = e.charCodeAt(t); if ( !f(i) && (!((n = i) >= a.UPPER_A) || !(n <= a.UPPER_F)) && (!(n >= a.LOWER_A) || !(n <= a.LOWER_F)) ) return ( this.addToNumericResult(e, r, t, 16), this.emitNumericEntity(i, 3) ); t += 1; } return this.addToNumericResult(e, r, t, 16), -1; } stateNumericDecimal(e, t) { let r = t; for (; t < e.length; ) { let n = e.charCodeAt(t); if (!f(n)) return ( this.addToNumericResult(e, r, t, 10), this.emitNumericEntity(n, 2) ); t += 1; } return this.addToNumericResult(e, r, t, 10), -1; } emitNumericEntity(e, t) { var r; if (this.consumed <= t) return ( null == (r = this.errors) || r.absenceOfDigitsInNumericCharacterReference(this.consumed), 0 ); if (e === a.SEMI) this.consumed += 1; else if (this.decodeMode === u.Strict) return 0; return ( this.emitCodePoint((0, p.y6)(this.result), this.consumed), this.errors && (e !== a.SEMI && this.errors.missingSemicolonAfterCharacterReference(), this.errors.validateNumericCharacterReference(this.result)), this.consumed ); } stateNamedEntity(e, t) { let { decodeTree: r } = this, n = r[this.treeIndex], i = (n & l.VALUE_LENGTH) >> 14; for (; t < e.length; t++, this.excess++) { let s = e.charCodeAt(t); if ( ((this.treeIndex = (function (e, t, r, n) { let i = (t & l.BRANCH_LENGTH) >> 7, s = t & l.JUMP_TABLE; if (0 === i) return 0 !== s && n === s ? r : -1; if (s) { let t = n - s; return t < 0 || t >= i ? -1 : e[r + t] - 1; } let o = r, a = o + i - 1; for (; o <= a; ) { let t = (o + a) >>> 1, r = e[t]; if (r < n) o = t + 1; else { if (!(r > n)) return e[t + i]; a = t - 1; } } return -1; })(r, n, this.treeIndex + Math.max(1, i), s)), this.treeIndex < 0) ) return 0 === this.result || (this.decodeMode === u.Attribute && (0 === i || (function (e) { var t; return ( e === a.EQUALS || ((t = e) >= a.UPPER_A && t <= a.UPPER_Z) || (t >= a.LOWER_A && t <= a.LOWER_Z) || f(t) ); })(s))) ? 0 : this.emitNotTerminatedNamedEntity(); if (0 != (i = ((n = r[this.treeIndex]) & l.VALUE_LENGTH) >> 14)) { if (s === a.SEMI) return this.emitNamedEntityData( this.treeIndex, i, this.consumed + this.excess ); this.decodeMode !== u.Strict && ((this.result = this.treeIndex), (this.consumed += this.excess), (this.excess = 0)); } } return -1; } emitNotTerminatedNamedEntity() { var e; let { result: t, decodeTree: r } = this, n = (r[t] & l.VALUE_LENGTH) >> 14; return ( this.emitNamedEntityData(t, n, this.consumed), null == (e = this.errors) || e.missingSemicolonAfterCharacterReference(), this.consumed ); } emitNamedEntityData(e, t, r) { let { decodeTree: n } = this; return ( this.emitCodePoint( 1 === t ? n[e] & ~l.VALUE_LENGTH : n[e + 1], r ), 3 === t && this.emitCodePoint(n[e + 2], r), r ); } end() { var e; switch (this.state) { case c.NamedEntity: return 0 !== this.result && (this.decodeMode !== u.Attribute || this.result === this.treeIndex) ? this.emitNotTerminatedNamedEntity() : 0; case c.NumericDecimal: return this.emitNumericEntity(0, 2); case c.NumericHex: return this.emitNumericEntity(0, 3); case c.NumericStart: return ( null == (e = this.errors) || e.absenceOfDigitsInNumericCharacterReference(this.consumed), 0 ); case c.EntityStart: return 0; } } } }, 466: function (e, t, r) { r(9496), r(747); }, 747: function (e, t, r) { r.d(t, { Gj: () => l, WY: () => o, X1: () => c }); let n = /["$&'<>\u0080-\uFFFF]/g, i = new Map([ [34, """], [38, "&"], [39, "'"], [60, "<"], [62, ">"], ]), s = null == String.prototype.codePointAt ? (e, t) => (64512 & e.charCodeAt(t)) == 55296 ? (e.charCodeAt(t) - 55296) * 1024 + e.charCodeAt(t + 1) - 56320 + 65536 : e.charCodeAt(t) : (e, t) => e.codePointAt(t); function o(e) { let t, r = "", o = 0; for (; null !== (t = n.exec(e)); ) { let { index: a } = t, l = e.charCodeAt(a), c = i.get(l); void 0 === c ? ((r += `${e.substring(o, a)}&#x${s(e, a).toString(16)};`), (o = n.lastIndex += Number((64512 & l) == 55296))) : ((r += e.substring(o, a) + c), (o = a + 1)); } return r + e.substr(o); } function a(e, t) { return function (r) { let n, i = 0, s = ""; for (; (n = e.exec(r)); ) i !== n.index && (s += r.substring(i, n.index)), (s += t.get(n[0].charCodeAt(0))), (i = n.index + 1); return s + r.substring(i); }; } let l = a( /["&\u00A0]/g, new Map([ [34, """], [38, "&"], [160, " "], ]) ), c = a( /[&<>\u00A0]/g, new Map([ [38, "&"], [60, "<"], [62, ">"], [160, " "], ]) ); }, 7259: function (e, t, r) { r.d(t, { q: () => n }); let n = new Uint16Array( 'ᵁ<\xd5ıʊҝջאٵ۞ޢߖࠏ੊ઑඡ๭༉༦჊ረዡᐕᒝᓃᓟᔥ\0\0\0\0\0\0ᕫᛍᦍᰒᷝ὾⁠↰⊍⏀⏻⑂⠤⤒ⴈ⹈⿎〖㊺㘹㞬㣾㨨㩱㫠㬮ࠀEMabcfglmnoprstu\\bfms\x7f\x84\x8b\x90\x95\x98\xa6\xb3\xb9\xc8\xcflig耻\xc6䃆P耻&䀦cute耻\xc1䃁reve;䄂Āiyx}rc耻\xc2䃂;䐐r;쀀\ud835\udd04rave耻\xc0䃀pha;䎑acr;䄀d;橓Āgp\x9d\xa1on;䄄f;쀀\ud835\udd38plyFunction;恡ing耻\xc5䃅Ācs\xbe\xc3r;쀀\ud835\udc9cign;扔ilde耻\xc3䃃ml耻\xc4䃄Ѐaceforsu\xe5\xfb\xfeėĜĢħĪĀcr\xea\xf2kslash;或Ŷ\xf6\xf8;櫧ed;挆y;䐑ƀcrtąċĔause;戵noullis;愬a;䎒r;쀀\ud835\udd05pf;쀀\ud835\udd39eve;䋘c\xf2ēmpeq;扎܀HOacdefhilorsuōőŖƀƞƢƵƷƺǜȕɳɸɾcy;䐧PY耻\xa9䂩ƀcpyŝŢźute;䄆Ā;iŧŨ拒talDifferentialD;慅leys;愭ȀaeioƉƎƔƘron;䄌dil耻\xc7䃇rc;䄈nint;戰ot;䄊ĀdnƧƭilla;䂸terDot;䂷\xf2ſi;䎧rcleȀDMPTLJNjǑǖot;抙inus;抖lus;投imes;抗oĀcsǢǸkwiseContourIntegral;戲eCurlyĀDQȃȏoubleQuote;思uote;怙ȀlnpuȞȨɇɕonĀ;eȥȦ户;橴ƀgitȯȶȺruent;扡nt;戯ourIntegral;戮ĀfrɌɎ;愂oduct;成nterClockwiseContourIntegral;戳oss;樯cr;쀀\ud835\udc9epĀ;Cʄʅ拓ap;才րDJSZacefiosʠʬʰʴʸˋ˗ˡ˦̳ҍĀ;oŹʥtrahd;椑cy;䐂cy;䐅cy;䐏ƀgrsʿ˄ˇger;怡r;憡hv;櫤Āayː˕ron;䄎;䐔lĀ;t˝˞戇a;䎔r;쀀\ud835\udd07Āaf˫̧Ācm˰̢riticalȀADGT̖̜̀̆cute;䂴oŴ̋̍;䋙bleAcute;䋝rave;䁠ilde;䋜ond;拄ferentialD;慆Ѱ̽\0\0\0͔͂\0Ѕf;쀀\ud835\udd3bƀ;DE͈͉͍䂨ot;惜qual;扐blèCDLRUVͣͲ΂ϏϢϸontourIntegra\xecȹoɴ͹\0\0ͻ\xbb͉nArrow;懓Āeo·ΤftƀARTΐΖΡrrow;懐ightArrow;懔e\xe5ˊngĀLRΫτeftĀARγιrrow;柸ightArrow;柺ightArrow;柹ightĀATϘϞrrow;懒ee;抨pɁϩ\0\0ϯrrow;懑ownArrow;懕erticalBar;戥ǹABLRTaВЪаўѿͼrrowƀ;BUНОТ憓ar;椓pArrow;懵reve;䌑eft˒к\0ц\0ѐightVector;楐eeVector;楞ectorĀ;Bљњ憽ar;楖ightǔѧ\0ѱeeVector;楟ectorĀ;BѺѻ懁ar;楗eeĀ;A҆҇护rrow;憧ĀctҒҗr;쀀\ud835\udc9frok;䄐ࠀNTacdfglmopqstuxҽӀӄӋӞӢӧӮӵԡԯԶՒ՝ՠեG;䅊H耻\xd0䃐cute耻\xc9䃉ƀaiyӒӗӜron;䄚rc耻\xca䃊;䐭ot;䄖r;쀀\ud835\udd08rave耻\xc8䃈ement;戈ĀapӺӾcr;䄒tyɓԆ\0\0ԒmallSquare;旻erySmallSquare;斫ĀgpԦԪon;䄘f;쀀\ud835\udd3csilon;䎕uĀaiԼՉlĀ;TՂՃ橵ilde;扂librium;懌Āci՗՚r;愰m;橳a;䎗ml耻\xcb䃋Āipժկsts;戃onentialE;慇ʀcfiosօֈ֍ֲ׌y;䐤r;쀀\ud835\udd09lledɓ֗\0\0֣mallSquare;旼erySmallSquare;斪Ͱֺ\0ֿ\0\0ׄf;쀀\ud835\udd3dAll;戀riertrf;愱c\xf2׋؀JTabcdfgorstר׬ׯ׺؀ؒؖ؛؝أ٬ٲcy;䐃耻>䀾mmaĀ;d׷׸䎓;䏜reve;䄞ƀeiy؇،ؐdil;䄢rc;䄜;䐓ot;䄠r;쀀\ud835\udd0a;拙pf;쀀\ud835\udd3eeater̀EFGLSTصلَٖٛ٦qualĀ;Lؾؿ扥ess;招ullEqual;执reater;檢ess;扷lantEqual;橾ilde;扳cr;쀀\ud835\udca2;扫ЀAacfiosuڅڋږڛڞڪھۊRDcy;䐪Āctڐڔek;䋇;䁞irc;䄤r;愌lbertSpace;愋ǰگ\0ڲf;愍izontalLine;攀Āctۃۅ\xf2کrok;䄦mpńېۘownHum\xf0įqual;扏܀EJOacdfgmnostuۺ۾܃܇܎ܚܞܡܨ݄ݸދޏޕcy;䐕lig;䄲cy;䐁cute耻\xcd䃍Āiyܓܘrc耻\xce䃎;䐘ot;䄰r;愑rave耻\xcc䃌ƀ;apܠܯܿĀcgܴܷr;䄪inaryI;慈lie\xf3ϝǴ݉\0ݢĀ;eݍݎ戬Āgrݓݘral;戫section;拂isibleĀCTݬݲomma;恣imes;恢ƀgptݿރވon;䄮f;쀀\ud835\udd40a;䎙cr;愐ilde;䄨ǫޚ\0ޞcy;䐆l耻\xcf䃏ʀcfosuެ޷޼߂ߐĀiyޱ޵rc;䄴;䐙r;쀀\ud835\udd0dpf;쀀\ud835\udd41ǣ߇\0ߌr;쀀\ud835\udca5rcy;䐈kcy;䐄΀HJacfosߤߨ߽߬߱ࠂࠈcy;䐥cy;䐌ppa;䎚Āey߶߻dil;䄶;䐚r;쀀\ud835\udd0epf;쀀\ud835\udd42cr;쀀\ud835\udca6րJTaceflmostࠥࠩࠬࡐࡣ঳সে্਷ੇcy;䐉耻<䀼ʀcmnpr࠷࠼ࡁࡄࡍute;䄹bda;䎛g;柪lacetrf;愒r;憞ƀaeyࡗ࡜ࡡron;䄽dil;䄻;䐛Āfsࡨ॰tԀACDFRTUVarࡾࢩࢱࣦ࣠ࣼयज़ΐ४Ānrࢃ࢏gleBracket;柨rowƀ;BR࢙࢚࢞憐ar;懤ightArrow;懆eiling;挈oǵࢷ\0ࣃbleBracket;柦nǔࣈ\0࣒eeVector;楡ectorĀ;Bࣛࣜ懃ar;楙loor;挊ightĀAV࣯ࣵrrow;憔ector;楎Āerँगeƀ;AVउऊऐ抣rrow;憤ector;楚iangleƀ;BEतथऩ抲ar;槏qual;抴pƀDTVषूौownVector;楑eeVector;楠ectorĀ;Bॖॗ憿ar;楘ectorĀ;B॥०憼ar;楒ight\xe1Μs̀EFGLSTॾঋকঝঢভqualGreater;拚ullEqual;扦reater;扶ess;檡lantEqual;橽ilde;扲r;쀀\ud835\udd0fĀ;eঽা拘ftarrow;懚idot;䄿ƀnpw৔ਖਛgȀLRlr৞৷ਂਐeftĀAR০৬rrow;柵ightArrow;柷ightArrow;柶eftĀarγਊight\xe1οight\xe1ϊf;쀀\ud835\udd43erĀLRਢਬeftArrow;憙ightArrow;憘ƀchtਾੀੂ\xf2ࡌ;憰rok;䅁;扪Ѐacefiosuਗ਼੝੠੷੼અઋ઎p;椅y;䐜Ādl੥੯iumSpace;恟lintrf;愳r;쀀\ud835\udd10nusPlus;戓pf;쀀\ud835\udd44c\xf2੶;䎜ҀJacefostuણધભીଔଙඑ඗ඞcy;䐊cute;䅃ƀaey઴હાron;䅇dil;䅅;䐝ƀgswે૰଎ativeƀMTV૓૟૨ediumSpace;怋hiĀcn૦૘\xeb૙eryThi\xee૙tedĀGL૸ଆreaterGreate\xf2ٳessLes\xf3ੈLine;䀊r;쀀\ud835\udd11ȀBnptଢନଷ଺reak;恠BreakingSpace;䂠f;愕ڀ;CDEGHLNPRSTV୕ୖ୪୼஡௫ఄ౞಄ದ೘ൡඅ櫬Āou୛୤ngruent;扢pCap;扭oubleVerticalBar;戦ƀlqxஃஊ஛ement;戉ualĀ;Tஒஓ扠ilde;쀀≂̸ists;戄reater΀;EFGLSTஶஷ஽௉௓௘௥扯qual;扱ullEqual;쀀≧̸reater;쀀≫̸ess;批lantEqual;쀀⩾̸ilde;扵umpń௲௽ownHump;쀀≎̸qual;쀀≏̸eĀfsఊధtTriangleƀ;BEచఛడ拪ar;쀀⧏̸qual;括s̀;EGLSTవశ఼ౄోౘ扮qual;扰reater;扸ess;쀀≪̸lantEqual;쀀⩽̸ilde;扴estedĀGL౨౹reaterGreater;쀀⪢̸essLess;쀀⪡̸recedesƀ;ESಒಓಛ技qual;쀀⪯̸lantEqual;拠ĀeiಫಹverseElement;戌ghtTriangleƀ;BEೋೌ೒拫ar;쀀⧐̸qual;拭ĀquೝഌuareSuĀbp೨೹setĀ;E೰ೳ쀀⊏̸qual;拢ersetĀ;Eഃആ쀀⊐̸qual;拣ƀbcpഓതൎsetĀ;Eഛഞ쀀⊂⃒qual;抈ceedsȀ;ESTലള഻െ抁qual;쀀⪰̸lantEqual;拡ilde;쀀≿̸ersetĀ;E൘൛쀀⊃⃒qual;抉ildeȀ;EFT൮൯൵ൿ扁qual;扄ullEqual;扇ilde;扉erticalBar;戤cr;쀀\ud835\udca9ilde耻\xd1䃑;䎝܀Eacdfgmoprstuvලෂ෉෕ෛ෠෧෼ขภยา฿ไlig;䅒cute耻\xd3䃓Āiy෎ීrc耻\xd4䃔;䐞blac;䅐r;쀀\ud835\udd12rave耻\xd2䃒ƀaei෮ෲ෶cr;䅌ga;䎩cron;䎟pf;쀀\ud835\udd46enCurlyĀDQฎบoubleQuote;怜uote;怘;橔Āclวฬr;쀀\ud835\udcaaash耻\xd8䃘iŬื฼de耻\xd5䃕es;樷ml耻\xd6䃖erĀBP๋๠Āar๐๓r;怾acĀek๚๜;揞et;掴arenthesis;揜Ҁacfhilors๿ງຊຏຒດຝະ໼rtialD;戂y;䐟r;쀀\ud835\udd13i;䎦;䎠usMinus;䂱Āipຢອncareplan\xe5ڝf;愙Ȁ;eio຺ູ໠໤檻cedesȀ;EST່້໏໚扺qual;檯lantEqual;扼ilde;找me;怳Ādp໩໮uct;戏ortionĀ;aȥ໹l;戝Āci༁༆r;쀀\ud835\udcab;䎨ȀUfos༑༖༛༟OT耻"䀢r;쀀\ud835\udd14pf;愚cr;쀀\ud835\udcac؀BEacefhiorsu༾གྷཇའཱིྦྷྪྭ႖ႩႴႾarr;椐G耻\xae䂮ƀcnrཎནབute;䅔g;柫rĀ;tཛྷཝ憠l;椖ƀaeyཧཬཱron;䅘dil;䅖;䐠Ā;vླྀཹ愜erseĀEUྂྙĀlq྇ྎement;戋uilibrium;懋pEquilibrium;楯r\xbbཹo;䎡ghtЀACDFTUVa࿁࿫࿳ဢဨၛႇϘĀnr࿆࿒gleBracket;柩rowƀ;BL࿜࿝࿡憒ar;懥eftArrow;懄eiling;按oǵ࿹\0စbleBracket;柧nǔည\0နeeVector;楝ectorĀ;Bဝသ懂ar;楕loor;挋Āerိ၃eƀ;AVဵံြ抢rrow;憦ector;楛iangleƀ;BEၐၑၕ抳ar;槐qual;抵pƀDTVၣၮၸownVector;楏eeVector;楜ectorĀ;Bႂႃ憾ar;楔ectorĀ;B႑႒懀ar;楓Āpuႛ႞f;愝ndImplies;楰ightarrow;懛ĀchႹႼr;愛;憱leDelayed;槴ڀHOacfhimoqstuფჱჷჽᄙᄞᅑᅖᅡᅧᆵᆻᆿĀCcჩხHcy;䐩y;䐨FTcy;䐬cute;䅚ʀ;aeiyᄈᄉᄎᄓᄗ檼ron;䅠dil;䅞rc;䅜;䐡r;쀀\ud835\udd16ortȀDLRUᄪᄴᄾᅉownArrow\xbbОeftArrow\xbb࢚ightArrow\xbb࿝pArrow;憑gma;䎣allCircle;战pf;쀀\ud835\udd4aɲᅭ\0\0ᅰt;戚areȀ;ISUᅻᅼᆉᆯ斡ntersection;抓uĀbpᆏᆞsetĀ;Eᆗᆘ抏qual;抑ersetĀ;Eᆨᆩ抐qual;抒nion;抔cr;쀀\ud835\udcaear;拆ȀbcmpᇈᇛሉላĀ;sᇍᇎ拐etĀ;Eᇍᇕqual;抆ĀchᇠህeedsȀ;ESTᇭᇮᇴᇿ扻qual;檰lantEqual;扽ilde;承Th\xe1ྌ;我ƀ;esሒሓሣ拑rsetĀ;Eሜም抃qual;抇et\xbbሓրHRSacfhiorsሾቄ቉ቕ቞ቱቶኟዂወዑORN耻\xde䃞ADE;愢ĀHc቎ቒcy;䐋y;䐦Ābuቚቜ;䀉;䎤ƀaeyብቪቯron;䅤dil;䅢;䐢r;쀀\ud835\udd17Āeiቻ኉Dzኀ\0ኇefore;戴a;䎘Ācn኎ኘkSpace;쀀  Space;怉ldeȀ;EFTካኬኲኼ戼qual;扃ullEqual;扅ilde;扈pf;쀀\ud835\udd4bipleDot;惛Āctዖዛr;쀀\ud835\udcafrok;䅦ૡዷጎጚጦ\0ጬጱ\0\0\0\0\0ጸጽ፷ᎅ\0᏿ᐄᐊᐐĀcrዻጁute耻\xda䃚rĀ;oጇገ憟cir;楉rǣጓ\0጖y;䐎ve;䅬Āiyጞጣrc耻\xdb䃛;䐣blac;䅰r;쀀\ud835\udd18rave耻\xd9䃙acr;䅪Ādiፁ፩erĀBPፈ፝Āarፍፐr;䁟acĀekፗፙ;揟et;掵arenthesis;揝onĀ;P፰፱拃lus;抎Āgp፻፿on;䅲f;쀀\ud835\udd4cЀADETadps᎕ᎮᎸᏄϨᏒᏗᏳrrowƀ;BDᅐᎠᎤar;椒ownArrow;懅ownArrow;憕quilibrium;楮eeĀ;AᏋᏌ报rrow;憥own\xe1ϳerĀLRᏞᏨeftArrow;憖ightArrow;憗iĀ;lᏹᏺ䏒on;䎥ing;䅮cr;쀀\ud835\udcb0ilde;䅨ml耻\xdc䃜ҀDbcdefosvᐧᐬᐰᐳᐾᒅᒊᒐᒖash;披ar;櫫y;䐒ashĀ;lᐻᐼ抩;櫦Āerᑃᑅ;拁ƀbtyᑌᑐᑺar;怖Ā;iᑏᑕcalȀBLSTᑡᑥᑪᑴar;戣ine;䁼eparator;杘ilde;所ThinSpace;怊r;쀀\ud835\udd19pf;쀀\ud835\udd4dcr;쀀\ud835\udcb1dash;抪ʀcefosᒧᒬᒱᒶᒼirc;䅴dge;拀r;쀀\ud835\udd1apf;쀀\ud835\udd4ecr;쀀\ud835\udcb2Ȁfiosᓋᓐᓒᓘr;쀀\ud835\udd1b;䎞pf;쀀\ud835\udd4fcr;쀀\ud835\udcb3ҀAIUacfosuᓱᓵᓹᓽᔄᔏᔔᔚᔠcy;䐯cy;䐇cy;䐮cute耻\xdd䃝Āiyᔉᔍrc;䅶;䐫r;쀀\ud835\udd1cpf;쀀\ud835\udd50cr;쀀\ud835\udcb4ml;䅸ЀHacdefosᔵᔹᔿᕋᕏᕝᕠᕤcy;䐖cute;䅹Āayᕄᕉron;䅽;䐗ot;䅻Dzᕔ\0ᕛoWidt\xe8૙a;䎖r;愨pf;愤cr;쀀\ud835\udcb5௡ᖃᖊᖐ\0ᖰᖶᖿ\0\0\0\0ᗆᗛᗫᙟ᙭\0ᚕ᚛ᚲᚹ\0ᚾcute耻\xe1䃡reve;䄃̀;Ediuyᖜᖝᖡᖣᖨᖭ戾;쀀∾̳;房rc耻\xe2䃢te肻\xb4̆;䐰lig耻\xe6䃦Ā;r\xb2ᖺ;쀀\ud835\udd1erave耻\xe0䃠ĀepᗊᗖĀfpᗏᗔsym;愵\xe8ᗓha;䎱ĀapᗟcĀclᗤᗧr;䄁g;樿ɤᗰ\0\0ᘊʀ;adsvᗺᗻᗿᘁᘇ戧nd;橕;橜lope;橘;橚΀;elmrszᘘᘙᘛᘞᘿᙏᙙ戠;榤e\xbbᘙsdĀ;aᘥᘦ戡ѡᘰᘲᘴᘶᘸᘺᘼᘾ;榨;榩;榪;榫;榬;榭;榮;榯tĀ;vᙅᙆ戟bĀ;dᙌᙍ抾;榝Āptᙔᙗh;戢\xbb\xb9arr;捼Āgpᙣᙧon;䄅f;쀀\ud835\udd52΀;Eaeiop዁ᙻᙽᚂᚄᚇᚊ;橰cir;橯;扊d;手s;䀧roxĀ;e዁ᚒ\xf1ᚃing耻\xe5䃥ƀctyᚡᚦᚨr;쀀\ud835\udcb6;䀪mpĀ;e዁ᚯ\xf1ʈilde耻\xe3䃣ml耻\xe4䃤Āciᛂᛈonin\xf4ɲnt;樑ࠀNabcdefiklnoprsu᛭ᛱᜰ᜼ᝃᝈ᝸᝽០៦ᠹᡐᜍ᤽᥈ᥰot;櫭Ācrᛶ᜞kȀcepsᜀᜅᜍᜓong;扌psilon;䏶rime;怵imĀ;e᜚᜛戽q;拍Ŷᜢᜦee;抽edĀ;gᜬᜭ挅e\xbbᜭrkĀ;t፜᜷brk;掶Āoyᜁᝁ;䐱quo;怞ʀcmprtᝓ᝛ᝡᝤᝨausĀ;eĊĉptyv;榰s\xe9ᜌno\xf5ēƀahwᝯ᝱ᝳ;䎲;愶een;扬r;쀀\ud835\udd1fg΀costuvwឍឝឳេ៕៛៞ƀaiuបពរ\xf0ݠrc;旯p\xbb፱ƀdptឤឨឭot;樀lus;樁imes;樂ɱឹ\0\0ើcup;樆ar;昅riangleĀdu៍្own;施p;斳plus;樄e\xe5ᑄ\xe5ᒭarow;植ƀako៭ᠦᠵĀcn៲ᠣkƀlst៺֫᠂ozenge;槫riangleȀ;dlr᠒᠓᠘᠝斴own;斾eft;旂ight;斸k;搣Ʊᠫ\0ᠳƲᠯ\0ᠱ;斒;斑4;斓ck;斈ĀeoᠾᡍĀ;qᡃᡆ쀀=⃥uiv;쀀≡⃥t;挐Ȁptwxᡙᡞᡧᡬf;쀀\ud835\udd53Ā;tᏋᡣom\xbbᏌtie;拈؀DHUVbdhmptuvᢅᢖᢪᢻᣗᣛᣬ᣿ᤅᤊᤐᤡȀLRlrᢎᢐᢒᢔ;敗;敔;敖;敓ʀ;DUduᢡᢢᢤᢦᢨ敐;敦;敩;敤;敧ȀLRlrᢳᢵᢷᢹ;敝;敚;敜;教΀;HLRhlrᣊᣋᣍᣏᣑᣓᣕ救;敬;散;敠;敫;敢;敟ox;槉ȀLRlrᣤᣦᣨᣪ;敕;敒;攐;攌ʀ;DUduڽ᣷᣹᣻᣽;敥;敨;攬;攴inus;抟lus;択imes;抠ȀLRlrᤙᤛᤝ᤟;敛;敘;攘;攔΀;HLRhlrᤰᤱᤳᤵᤷ᤻᤹攂;敪;敡;敞;攼;攤;攜Āevģ᥂bar耻\xa6䂦Ȁceioᥑᥖᥚᥠr;쀀\ud835\udcb7mi;恏mĀ;e᜚᜜lƀ;bhᥨᥩᥫ䁜;槅sub;柈Ŭᥴ᥾lĀ;e᥹᥺怢t\xbb᥺pƀ;Eeįᦅᦇ;檮Ā;qۜۛೡᦧ\0᧨ᨑᨕᨲ\0ᨷᩐ\0\0᪴\0\0᫁\0\0ᬡᬮ᭍᭒\0᯽\0ᰌƀcpr᦭ᦲ᧝ute;䄇̀;abcdsᦿᧀᧄ᧊᧕᧙戩nd;橄rcup;橉Āau᧏᧒p;橋p;橇ot;橀;쀀∩︀Āeo᧢᧥t;恁\xeeړȀaeiu᧰᧻ᨁᨅǰ᧵\0᧸s;橍on;䄍dil耻\xe7䃧rc;䄉psĀ;sᨌᨍ橌m;橐ot;䄋ƀdmnᨛᨠᨦil肻\xb8ƭptyv;榲t脀\xa2;eᨭᨮ䂢r\xe4Ʋr;쀀\ud835\udd20ƀceiᨽᩀᩍy;䑇ckĀ;mᩇᩈ朓ark\xbbᩈ;䏇r΀;Ecefms᩟᩠ᩢᩫ᪤᪪᪮旋;槃ƀ;elᩩᩪᩭ䋆q;扗eɡᩴ\0\0᪈rrowĀlr᩼᪁eft;憺ight;憻ʀRSacd᪒᪔᪖᪚᪟\xbbཇ;擈st;抛irc;抚ash;抝nint;樐id;櫯cir;槂ubsĀ;u᪻᪼晣it\xbb᪼ˬ᫇᫔᫺\0ᬊonĀ;eᫍᫎ䀺Ā;q\xc7\xc6ɭ᫙\0\0᫢aĀ;t᫞᫟䀬;䁀ƀ;fl᫨᫩᫫戁\xeeᅠeĀmx᫱᫶ent\xbb᫩e\xf3ɍǧ᫾\0ᬇĀ;dኻᬂot;橭n\xf4Ɇƀfryᬐᬔᬗ;쀀\ud835\udd54o\xe4ɔ脀\xa9;sŕᬝr;愗Āaoᬥᬩrr;憵ss;朗Ācuᬲᬷr;쀀\ud835\udcb8Ābpᬼ᭄Ā;eᭁᭂ櫏;櫑Ā;eᭉᭊ櫐;櫒dot;拯΀delprvw᭠᭬᭷ᮂᮬᯔ᯹arrĀlr᭨᭪;椸;椵ɰ᭲\0\0᭵r;拞c;拟arrĀ;p᭿ᮀ憶;椽̀;bcdosᮏᮐᮖᮡᮥᮨ截rcap;橈Āauᮛᮞp;橆p;橊ot;抍r;橅;쀀∪︀Ȁalrv᮵ᮿᯞᯣrrĀ;mᮼᮽ憷;椼yƀevwᯇᯔᯘqɰᯎ\0\0ᯒre\xe3᭳u\xe3᭵ee;拎edge;拏en耻\xa4䂤earrowĀlrᯮ᯳eft\xbbᮀight\xbbᮽe\xe4ᯝĀciᰁᰇonin\xf4Ƿnt;戱lcty;挭ঀAHabcdefhijlorstuwz᰸᰻᰿ᱝᱩᱵᲊᲞᲬᲷ᳻᳿ᴍᵻᶑᶫᶻ᷆᷍r\xf2΁ar;楥Ȁglrs᱈ᱍ᱒᱔ger;怠eth;愸\xf2ᄳhĀ;vᱚᱛ怐\xbbऊūᱡᱧarow;椏a\xe3̕Āayᱮᱳron;䄏;䐴ƀ;ao̲ᱼᲄĀgrʿᲁr;懊tseq;橷ƀglmᲑᲔᲘ耻\xb0䂰ta;䎴ptyv;榱ĀirᲣᲨsht;楿;쀀\ud835\udd21arĀlrᲳᲵ\xbbࣜ\xbbသʀaegsv᳂͸᳖᳜᳠mƀ;oș᳊᳔ndĀ;ș᳑uit;晦amma;䏝in;拲ƀ;io᳧᳨᳸䃷de脀\xf7;o᳧ᳰntimes;拇n\xf8᳷cy;䑒cɯᴆ\0\0ᴊrn;挞op;挍ʀlptuwᴘᴝᴢᵉᵕlar;䀤f;쀀\ud835\udd55ʀ;emps̋ᴭᴷᴽᵂqĀ;d͒ᴳot;扑inus;戸lus;戔quare;抡blebarwedg\xe5\xfanƀadhᄮᵝᵧownarrow\xf3ᲃarpoonĀlrᵲᵶef\xf4Ჴigh\xf4ᲶŢᵿᶅkaro\xf7གɯᶊ\0\0ᶎrn;挟op;挌ƀcotᶘᶣᶦĀryᶝᶡ;쀀\ud835\udcb9;䑕l;槶rok;䄑Ādrᶰᶴot;拱iĀ;fᶺ᠖斿Āah᷀᷃r\xf2Щa\xf2ྦangle;榦Āci᷒ᷕy;䑟grarr;柿ऀDacdefglmnopqrstuxḁḉḙḸոḼṉṡṾấắẽỡἪἷὄ὎὚ĀDoḆᴴo\xf4ᲉĀcsḎḔute耻\xe9䃩ter;橮ȀaioyḢḧḱḶron;䄛rĀ;cḭḮ扖耻\xea䃪lon;払;䑍ot;䄗ĀDrṁṅot;扒;쀀\ud835\udd22ƀ;rsṐṑṗ檚ave耻\xe8䃨Ā;dṜṝ檖ot;檘Ȁ;ilsṪṫṲṴ檙nters;揧;愓Ā;dṹṺ檕ot;檗ƀapsẅẉẗcr;䄓tyƀ;svẒẓẕ戅et\xbbẓpĀ1;ẝẤijạả;怄;怅怃ĀgsẪẬ;䅋p;怂ĀgpẴẸon;䄙f;쀀\ud835\udd56ƀalsỄỎỒrĀ;sỊị拕l;槣us;橱iƀ;lvỚớở䎵on\xbbớ;䏵ȀcsuvỪỳἋἣĀioữḱrc\xbbḮɩỹ\0\0ỻ\xedՈantĀglἂἆtr\xbbṝess\xbbṺƀaeiἒ἖Ἒls;䀽st;扟vĀ;DȵἠD;橸parsl;槥ĀDaἯἳot;打rr;楱ƀcdiἾὁỸr;愯o\xf4͒ĀahὉὋ;䎷耻\xf0䃰Āmrὓὗl耻\xeb䃫o;悬ƀcipὡὤὧl;䀡s\xf4ծĀeoὬὴctatio\xeeՙnential\xe5չৡᾒ\0ᾞ\0ᾡᾧ\0\0ῆῌ\0ΐ\0ῦῪ \0 ⁚llingdotse\xf1Ṅy;䑄male;晀ƀilrᾭᾳ῁lig;耀ffiɩᾹ\0\0᾽g;耀ffig;耀ffl;쀀\ud835\udd23lig;耀filig;쀀fjƀaltῙ῜ῡt;晭ig;耀flns;斱of;䆒ǰ΅\0ῳf;쀀\ud835\udd57ĀakֿῷĀ;vῼ´拔;櫙artint;樍Āao‌⁕Ācs‑⁒ႉ‸⁅⁈\0⁐β•‥‧‪‬\0‮耻\xbd䂽;慓耻\xbc䂼;慕;慙;慛Ƴ‴\0‶;慔;慖ʴ‾⁁\0\0⁃耻\xbe䂾;慗;慜5;慘ƶ⁌\0⁎;慚;慝8;慞l;恄wn;挢cr;쀀\ud835\udcbbࢀEabcdefgijlnorstv₂₉₟₥₰₴⃰⃵⃺⃿℃ℒℸ̗ℾ⅒↞Ā;lٍ₇;檌ƀcmpₐₕ₝ute;䇵maĀ;dₜ᳚䎳;檆reve;䄟Āiy₪₮rc;䄝;䐳ot;䄡Ȁ;lqsؾق₽⃉ƀ;qsؾٌ⃄lan\xf4٥Ȁ;cdl٥⃒⃥⃕c;檩otĀ;o⃜⃝檀Ā;l⃢⃣檂;檄Ā;e⃪⃭쀀⋛︀s;檔r;쀀\ud835\udd24Ā;gٳ؛mel;愷cy;䑓Ȁ;Eajٚℌℎℐ;檒;檥;檤ȀEaesℛℝ℩ℴ;扩pĀ;p℣ℤ檊rox\xbbℤĀ;q℮ℯ檈Ā;q℮ℛim;拧pf;쀀\ud835\udd58Āci⅃ⅆr;愊mƀ;el٫ⅎ⅐;檎;檐茀>;cdlqr׮ⅠⅪⅮⅳⅹĀciⅥⅧ;檧r;橺ot;拗Par;榕uest;橼ʀadelsↄⅪ←ٖ↛ǰ↉\0↎pro\xf8₞r;楸qĀlqؿ↖les\xf3₈i\xed٫Āen↣↭rtneqq;쀀≩︀\xc5↪ԀAabcefkosy⇄⇇⇱⇵⇺∘∝∯≨≽r\xf2ΠȀilmr⇐⇔⇗⇛rs\xf0ᒄf\xbb․il\xf4کĀdr⇠⇤cy;䑊ƀ;cwࣴ⇫⇯ir;楈;憭ar;意irc;䄥ƀalr∁∎∓rtsĀ;u∉∊晥it\xbb∊lip;怦con;抹r;쀀\ud835\udd25sĀew∣∩arow;椥arow;椦ʀamopr∺∾≃≞≣rr;懿tht;戻kĀlr≉≓eftarrow;憩ightarrow;憪f;쀀\ud835\udd59bar;怕ƀclt≯≴≸r;쀀\ud835\udcbdas\xe8⇴rok;䄧Ābp⊂⊇ull;恃hen\xbbᱛૡ⊣\0⊪\0⊸⋅⋎\0⋕⋳\0\0⋸⌢⍧⍢⍿\0⎆⎪⎴cute耻\xed䃭ƀ;iyݱ⊰⊵rc耻\xee䃮;䐸Ācx⊼⊿y;䐵cl耻\xa1䂡ĀfrΟ⋉;쀀\ud835\udd26rave耻\xec䃬Ȁ;inoܾ⋝⋩⋮Āin⋢⋦nt;樌t;戭fin;槜ta;愩lig;䄳ƀaop⋾⌚⌝ƀcgt⌅⌈⌗r;䄫ƀelpܟ⌏⌓in\xe5ގar\xf4ܠh;䄱f;抷ed;䆵ʀ;cfotӴ⌬⌱⌽⍁are;愅inĀ;t⌸⌹戞ie;槝do\xf4⌙ʀ;celpݗ⍌⍐⍛⍡al;抺Āgr⍕⍙er\xf3ᕣ\xe3⍍arhk;樗rod;樼Ȁcgpt⍯⍲⍶⍻y;䑑on;䄯f;쀀\ud835\udd5aa;䎹uest耻\xbf䂿Āci⎊⎏r;쀀\ud835\udcbenʀ;EdsvӴ⎛⎝⎡ӳ;拹ot;拵Ā;v⎦⎧拴;拳Ā;iݷ⎮lde;䄩ǫ⎸\0⎼cy;䑖l耻\xef䃯̀cfmosu⏌⏗⏜⏡⏧⏵Āiy⏑⏕rc;䄵;䐹r;쀀\ud835\udd27ath;䈷pf;쀀\ud835\udd5bǣ⏬\0⏱r;쀀\ud835\udcbfrcy;䑘kcy;䑔Ѐacfghjos␋␖␢␧␭␱␵␻ppaĀ;v␓␔䎺;䏰Āey␛␠dil;䄷;䐺r;쀀\ud835\udd28reen;䄸cy;䑅cy;䑜pf;쀀\ud835\udd5ccr;쀀\ud835\udcc0஀ABEHabcdefghjlmnoprstuv⑰⒁⒆⒍⒑┎┽╚▀♎♞♥♹♽⚚⚲⛘❝❨➋⟀⠁⠒ƀart⑷⑺⑼r\xf2৆\xf2Εail;椛arr;椎Ā;gঔ⒋;檋ar;楢ॣ⒥\0⒪\0⒱\0\0\0\0\0⒵Ⓔ\0ⓆⓈⓍ\0⓹ute;䄺mptyv;榴ra\xeeࡌbda;䎻gƀ;dlࢎⓁⓃ;榑\xe5ࢎ;檅uo耻\xab䂫rЀ;bfhlpst࢙ⓞⓦⓩ⓫⓮⓱⓵Ā;f࢝ⓣs;椟s;椝\xeb≒p;憫l;椹im;楳l;憢ƀ;ae⓿─┄檫il;椙Ā;s┉┊檭;쀀⪭︀ƀabr┕┙┝rr;椌rk;杲Āak┢┬cĀek┨┪;䁻;䁛Āes┱┳;榋lĀdu┹┻;榏;榍Ȁaeuy╆╋╖╘ron;䄾Ādi═╔il;䄼\xecࢰ\xe2┩;䐻Ȁcqrs╣╦╭╽a;椶uoĀ;rนᝆĀdu╲╷har;楧shar;楋h;憲ʀ;fgqs▋▌উ◳◿扤tʀahlrt▘▤▷◂◨rrowĀ;t࢙□a\xe9⓶arpoonĀdu▯▴own\xbbњp\xbb०eftarrows;懇ightƀahs◍◖◞rrowĀ;sࣴࢧarpoon\xf3྘quigarro\xf7⇰hreetimes;拋ƀ;qs▋ও◺lan\xf4বʀ;cdgsব☊☍☝☨c;檨otĀ;o☔☕橿Ā;r☚☛檁;檃Ā;e☢☥쀀⋚︀s;檓ʀadegs☳☹☽♉♋ppro\xf8Ⓠot;拖qĀgq♃♅\xf4উgt\xf2⒌\xf4ছi\xedলƀilr♕࣡♚sht;楼;쀀\ud835\udd29Ā;Eজ♣;檑š♩♶rĀdu▲♮Ā;l॥♳;楪lk;斄cy;䑙ʀ;achtੈ⚈⚋⚑⚖r\xf2◁orne\xf2ᴈard;楫ri;旺Āio⚟⚤dot;䅀ustĀ;a⚬⚭掰che\xbb⚭ȀEaes⚻⚽⛉⛔;扨pĀ;p⛃⛄檉rox\xbb⛄Ā;q⛎⛏檇Ā;q⛎⚻im;拦Ѐabnoptwz⛩⛴⛷✚✯❁❇❐Ānr⛮⛱g;柬r;懽r\xebࣁgƀlmr⛿✍✔eftĀar০✇ight\xe1৲apsto;柼ight\xe1৽parrowĀlr✥✩ef\xf4⓭ight;憬ƀafl✶✹✽r;榅;쀀\ud835\udd5dus;樭imes;樴š❋❏st;戗\xe1ፎƀ;ef❗❘᠀旊nge\xbb❘arĀ;l❤❥䀨t;榓ʀachmt❳❶❼➅➇r\xf2ࢨorne\xf2ᶌarĀ;d྘➃;業;怎ri;抿̀achiqt➘➝ੀ➢➮➻quo;怹r;쀀\ud835\udcc1mƀ;egল➪➬;檍;檏Ābu┪➳oĀ;rฟ➹;怚rok;䅂萀<;cdhilqrࠫ⟒☹⟜⟠⟥⟪⟰Āci⟗⟙;檦r;橹re\xe5◲mes;拉arr;楶uest;橻ĀPi⟵⟹ar;榖ƀ;ef⠀भ᠛旃rĀdu⠇⠍shar;楊har;楦Āen⠗⠡rtneqq;쀀≨︀\xc5⠞܀Dacdefhilnopsu⡀⡅⢂⢎⢓⢠⢥⢨⣚⣢⣤ઃ⣳⤂Dot;戺Ȁclpr⡎⡒⡣⡽r耻\xaf䂯Āet⡗⡙;時Ā;e⡞⡟朠se\xbb⡟Ā;sျ⡨toȀ;dluျ⡳⡷⡻ow\xeeҌef\xf4ए\xf0Ꮡker;斮Āoy⢇⢌mma;権;䐼ash;怔asuredangle\xbbᘦr;쀀\ud835\udd2ao;愧ƀcdn⢯⢴⣉ro耻\xb5䂵Ȁ;acdᑤ⢽⣀⣄s\xf4ᚧir;櫰ot肻\xb7Ƶusƀ;bd⣒ᤃ⣓戒Ā;uᴼ⣘;横ţ⣞⣡p;櫛\xf2−\xf0ઁĀdp⣩⣮els;抧f;쀀\ud835\udd5eĀct⣸⣽r;쀀\ud835\udcc2pos\xbbᖝƀ;lm⤉⤊⤍䎼timap;抸ఀGLRVabcdefghijlmoprstuvw⥂⥓⥾⦉⦘⧚⧩⨕⨚⩘⩝⪃⪕⪤⪨⬄⬇⭄⭿⮮ⰴⱧⱼ⳩Āgt⥇⥋;쀀⋙̸Ā;v⥐௏쀀≫⃒ƀelt⥚⥲⥶ftĀar⥡⥧rrow;懍ightarrow;懎;쀀⋘̸Ā;v⥻ే쀀≪⃒ightarrow;懏ĀDd⦎⦓ash;抯ash;抮ʀbcnpt⦣⦧⦬⦱⧌la\xbb˞ute;䅄g;쀀∠⃒ʀ;Eiop඄⦼⧀⧅⧈;쀀⩰̸d;쀀≋̸s;䅉ro\xf8඄urĀ;a⧓⧔普lĀ;s⧓ସdz⧟\0⧣p肻\xa0ଷmpĀ;e௹ఀʀaeouy⧴⧾⨃⨐⨓ǰ⧹\0⧻;橃on;䅈dil;䅆ngĀ;dൾ⨊ot;쀀⩭̸p;橂;䐽ash;怓΀;Aadqsxஒ⨩⨭⨻⩁⩅⩐rr;懗rĀhr⨳⨶k;椤Ā;oᏲᏰot;쀀≐̸ui\xf6ୣĀei⩊⩎ar;椨\xed஘istĀ;s஠டr;쀀\ud835\udd2bȀEest௅⩦⩹⩼ƀ;qs஼⩭௡ƀ;qs஼௅⩴lan\xf4௢i\xed௪Ā;rஶ⪁\xbbஷƀAap⪊⪍⪑r\xf2⥱rr;憮ar;櫲ƀ;svྍ⪜ྌĀ;d⪡⪢拼;拺cy;䑚΀AEadest⪷⪺⪾⫂⫅⫶⫹r\xf2⥦;쀀≦̸rr;憚r;急Ȁ;fqs఻⫎⫣⫯tĀar⫔⫙rro\xf7⫁ightarro\xf7⪐ƀ;qs఻⪺⫪lan\xf4ౕĀ;sౕ⫴\xbbశi\xedౝĀ;rవ⫾iĀ;eచథi\xe4ඐĀpt⬌⬑f;쀀\ud835\udd5f膀\xac;in⬙⬚⬶䂬nȀ;Edvஉ⬤⬨⬮;쀀⋹̸ot;쀀⋵̸ǡஉ⬳⬵;拷;拶iĀ;vಸ⬼ǡಸ⭁⭃;拾;拽ƀaor⭋⭣⭩rȀ;ast୻⭕⭚⭟lle\xec୻l;쀀⫽⃥;쀀∂̸lint;樔ƀ;ceಒ⭰⭳u\xe5ಥĀ;cಘ⭸Ā;eಒ⭽\xf1ಘȀAait⮈⮋⮝⮧r\xf2⦈rrƀ;cw⮔⮕⮙憛;쀀⤳̸;쀀↝̸ghtarrow\xbb⮕riĀ;eೋೖ΀chimpqu⮽⯍⯙⬄୸⯤⯯Ȁ;cerല⯆ഷ⯉u\xe5൅;쀀\ud835\udcc3ortɭ⬅\0\0⯖ar\xe1⭖mĀ;e൮⯟Ā;q൴൳suĀbp⯫⯭\xe5೸\xe5ഋƀbcp⯶ⰑⰙȀ;Ees⯿ⰀഢⰄ抄;쀀⫅̸etĀ;eഛⰋqĀ;qണⰀcĀ;eലⰗ\xf1സȀ;EesⰢⰣൟⰧ抅;쀀⫆̸etĀ;e൘ⰮqĀ;qൠⰣȀgilrⰽⰿⱅⱇ\xecௗlde耻\xf1䃱\xe7ృiangleĀlrⱒⱜeftĀ;eచⱚ\xf1దightĀ;eೋⱥ\xf1೗Ā;mⱬⱭ䎽ƀ;esⱴⱵⱹ䀣ro;愖p;怇ҀDHadgilrsⲏⲔⲙⲞⲣⲰⲶⳓⳣash;抭arr;椄p;쀀≍⃒ash;抬ĀetⲨⲬ;쀀≥⃒;쀀>⃒nfin;槞ƀAetⲽⳁⳅrr;椂;쀀≤⃒Ā;rⳊⳍ쀀<⃒ie;쀀⊴⃒ĀAtⳘⳜrr;椃rie;쀀⊵⃒im;쀀∼⃒ƀAan⳰⳴ⴂrr;懖rĀhr⳺⳽k;椣Ā;oᏧᏥear;椧ቓ᪕\0\0\0\0\0\0\0\0\0\0\0\0\0ⴭ\0ⴸⵈⵠⵥ⵲ⶄᬇ\0\0ⶍⶫ\0ⷈⷎ\0ⷜ⸙⸫⸾⹃Ācsⴱ᪗ute耻\xf3䃳ĀiyⴼⵅrĀ;c᪞ⵂ耻\xf4䃴;䐾ʀabios᪠ⵒⵗLjⵚlac;䅑v;樸old;榼lig;䅓Ācr⵩⵭ir;榿;쀀\ud835\udd2cͯ⵹\0\0⵼\0ⶂn;䋛ave耻\xf2䃲;槁Ābmⶈ෴ar;榵Ȁacitⶕ⶘ⶥⶨr\xf2᪀Āir⶝ⶠr;榾oss;榻n\xe5๒;槀ƀaeiⶱⶵⶹcr;䅍ga;䏉ƀcdnⷀⷅǍron;䎿;榶pf;쀀\ud835\udd60ƀaelⷔ⷗ǒr;榷rp;榹΀;adiosvⷪⷫⷮ⸈⸍⸐⸖戨r\xf2᪆Ȁ;efmⷷⷸ⸂⸅橝rĀ;oⷾⷿ愴f\xbbⷿ耻\xaa䂪耻\xba䂺gof;抶r;橖lope;橗;橛ƀclo⸟⸡⸧\xf2⸁ash耻\xf8䃸l;折iŬⸯ⸴de耻\xf5䃵esĀ;aǛ⸺s;樶ml耻\xf6䃶bar;挽ૡ⹞\0⹽\0⺀⺝\0⺢⺹\0\0⻋ຜ\0⼓\0\0⼫⾼\0⿈rȀ;astЃ⹧⹲຅脀\xb6;l⹭⹮䂶le\xecЃɩ⹸\0\0⹻m;櫳;櫽y;䐿rʀcimpt⺋⺏⺓ᡥ⺗nt;䀥od;䀮il;怰enk;怱r;쀀\ud835\udd2dƀimo⺨⺰⺴Ā;v⺭⺮䏆;䏕ma\xf4੶ne;明ƀ;tv⺿⻀⻈䏀chfork\xbb´;䏖Āau⻏⻟nĀck⻕⻝kĀ;h⇴⻛;愎\xf6⇴sҀ;abcdemst⻳⻴ᤈ⻹⻽⼄⼆⼊⼎䀫cir;樣ir;樢Āouᵀ⼂;樥;橲n肻\xb1ຝim;樦wo;樧ƀipu⼙⼠⼥ntint;樕f;쀀\ud835\udd61nd耻\xa3䂣Ԁ;Eaceinosu່⼿⽁⽄⽇⾁⾉⾒⽾⾶;檳p;檷u\xe5໙Ā;c໎⽌̀;acens່⽙⽟⽦⽨⽾ppro\xf8⽃urlye\xf1໙\xf1໎ƀaes⽯⽶⽺pprox;檹qq;檵im;拨i\xedໟmeĀ;s⾈ຮ怲ƀEas⽸⾐⽺\xf0⽵ƀdfp໬⾙⾯ƀals⾠⾥⾪lar;挮ine;挒urf;挓Ā;t໻⾴\xef໻rel;抰Āci⿀⿅r;쀀\ud835\udcc5;䏈ncsp;怈̀fiopsu⿚⋢⿟⿥⿫⿱r;쀀\ud835\udd2epf;쀀\ud835\udd62rime;恗cr;쀀\ud835\udcc6ƀaeo⿸〉〓tĀei⿾々rnion\xf3ڰnt;樖stĀ;e【】䀿\xf1Ἑ\xf4༔઀ABHabcdefhilmnoprstux぀けさすムㄎㄫㅇㅢㅲㆎ㈆㈕㈤㈩㉘㉮㉲㊐㊰㊷ƀartぇおがr\xf2Ⴓ\xf2ϝail;検ar\xf2ᱥar;楤΀cdenqrtとふへみわゔヌĀeuねぱ;쀀∽̱te;䅕i\xe3ᅮmptyv;榳gȀ;del࿑らるろ;榒;榥\xe5࿑uo耻\xbb䂻rր;abcfhlpstw࿜ガクシスゼゾダッデナp;極Ā;f࿠ゴs;椠;椳s;椞\xeb≝\xf0✮l;楅im;楴l;憣;憝Āaiパフil;椚oĀ;nホボ戶al\xf3༞ƀabrョリヮr\xf2៥rk;杳ĀakンヽcĀekヹ・;䁽;䁝Āes㄂㄄;榌lĀduㄊㄌ;榎;榐Ȁaeuyㄗㄜㄧㄩron;䅙Ādiㄡㄥil;䅗\xec࿲\xe2ヺ;䑀Ȁclqsㄴㄷㄽㅄa;椷dhar;楩uoĀ;rȎȍh;憳ƀacgㅎㅟངlȀ;ipsླྀㅘㅛႜn\xe5Ⴛar\xf4ྩt;断ƀilrㅩဣㅮsht;楽;쀀\ud835\udd2fĀaoㅷㆆrĀduㅽㅿ\xbbѻĀ;l႑ㆄ;楬Ā;vㆋㆌ䏁;䏱ƀgns㆕ㇹㇼht̀ahlrstㆤㆰ㇂㇘㇤㇮rrowĀ;t࿜ㆭa\xe9トarpoonĀduㆻㆿow\xeeㅾp\xbb႒eftĀah㇊㇐rrow\xf3࿪arpoon\xf3Ցightarrows;應quigarro\xf7ニhreetimes;拌g;䋚ingdotse\xf1ἲƀahm㈍㈐㈓r\xf2࿪a\xf2Ց;怏oustĀ;a㈞㈟掱che\xbb㈟mid;櫮Ȁabpt㈲㈽㉀㉒Ānr㈷㈺g;柭r;懾r\xebဃƀafl㉇㉊㉎r;榆;쀀\ud835\udd63us;樮imes;樵Āap㉝㉧rĀ;g㉣㉤䀩t;榔olint;樒ar\xf2㇣Ȁachq㉻㊀Ⴜ㊅quo;怺r;쀀\ud835\udcc7Ābu・㊊oĀ;rȔȓƀhir㊗㊛㊠re\xe5ㇸmes;拊iȀ;efl㊪ၙᠡ㊫方tri;槎luhar;楨;愞ൡ㋕㋛㋟㌬㌸㍱\0㍺㎤\0\0㏬㏰\0㐨㑈㑚㒭㒱㓊㓱\0㘖\0\0㘳cute;䅛qu\xef➺Ԁ;Eaceinpsyᇭ㋳㋵㋿㌂㌋㌏㌟㌦㌩;檴ǰ㋺\0㋼;檸on;䅡u\xe5ᇾĀ;dᇳ㌇il;䅟rc;䅝ƀEas㌖㌘㌛;檶p;檺im;择olint;樓i\xedሄ;䑁otƀ;be㌴ᵇ㌵担;橦΀Aacmstx㍆㍊㍗㍛㍞㍣㍭rr;懘rĀhr㍐㍒\xeb∨Ā;oਸ਼਴t耻\xa7䂧i;䀻war;椩mĀin㍩\xf0nu\xf3\xf1t;朶rĀ;o㍶⁕쀀\ud835\udd30Ȁacoy㎂㎆㎑㎠rp;景Āhy㎋㎏cy;䑉;䑈rtɭ㎙\0\0㎜i\xe4ᑤara\xec⹯耻\xad䂭Āgm㎨㎴maƀ;fv㎱㎲㎲䏃;䏂Ѐ;deglnprካ㏅㏉㏎㏖㏞㏡㏦ot;橪Ā;q኱ኰĀ;E㏓㏔檞;檠Ā;E㏛㏜檝;檟e;扆lus;樤arr;楲ar\xf2ᄽȀaeit㏸㐈㐏㐗Āls㏽㐄lsetm\xe9㍪hp;樳parsl;槤Ādlᑣ㐔e;挣Ā;e㐜㐝檪Ā;s㐢㐣檬;쀀⪬︀ƀflp㐮㐳㑂tcy;䑌Ā;b㐸㐹䀯Ā;a㐾㐿槄r;挿f;쀀\ud835\udd64aĀdr㑍ЂesĀ;u㑔㑕晠it\xbb㑕ƀcsu㑠㑹㒟Āau㑥㑯pĀ;sᆈ㑫;쀀⊓︀pĀ;sᆴ㑵;쀀⊔︀uĀbp㑿㒏ƀ;esᆗᆜ㒆etĀ;eᆗ㒍\xf1ᆝƀ;esᆨᆭ㒖etĀ;eᆨ㒝\xf1ᆮƀ;afᅻ㒦ְrť㒫ֱ\xbbᅼar\xf2ᅈȀcemt㒹㒾㓂㓅r;쀀\ud835\udcc8tm\xee\xf1i\xec㐕ar\xe6ᆾĀar㓎㓕rĀ;f㓔ឿ昆Āan㓚㓭ightĀep㓣㓪psilo\xeeỠh\xe9⺯s\xbb⡒ʀbcmnp㓻㕞ሉ㖋㖎Ҁ;Edemnprs㔎㔏㔑㔕㔞㔣㔬㔱㔶抂;櫅ot;檽Ā;dᇚ㔚ot;櫃ult;櫁ĀEe㔨㔪;櫋;把lus;檿arr;楹ƀeiu㔽㕒㕕tƀ;en㔎㕅㕋qĀ;qᇚ㔏eqĀ;q㔫㔨m;櫇Ābp㕚㕜;櫕;櫓c̀;acensᇭ㕬㕲㕹㕻㌦ppro\xf8㋺urlye\xf1ᇾ\xf1ᇳƀaes㖂㖈㌛ppro\xf8㌚q\xf1㌗g;晪ڀ123;Edehlmnps㖩㖬㖯ሜ㖲㖴㗀㗉㗕㗚㗟㗨㗭耻\xb9䂹耻\xb2䂲耻\xb3䂳;櫆Āos㖹㖼t;檾ub;櫘Ā;dሢ㗅ot;櫄sĀou㗏㗒l;柉b;櫗arr;楻ult;櫂ĀEe㗤㗦;櫌;抋lus;櫀ƀeiu㗴㘉㘌tƀ;enሜ㗼㘂qĀ;qሢ㖲eqĀ;q㗧㗤m;櫈Ābp㘑㘓;櫔;櫖ƀAan㘜㘠㘭rr;懙rĀhr㘦㘨\xeb∮Ā;oਫ਩war;椪lig耻\xdf䃟௡㙑㙝㙠ዎ㙳㙹\0㙾㛂\0\0\0\0\0㛛㜃\0㜉㝬\0\0\0㞇ɲ㙖\0\0㙛get;挖;䏄r\xeb๟ƀaey㙦㙫㙰ron;䅥dil;䅣;䑂lrec;挕r;쀀\ud835\udd31Ȁeiko㚆㚝㚵㚼Dz㚋\0㚑eĀ4fኄኁaƀ;sv㚘㚙㚛䎸ym;䏑Ācn㚢㚲kĀas㚨㚮ppro\xf8዁im\xbbኬs\xf0ኞĀas㚺㚮\xf0዁rn耻\xfe䃾Ǭ̟㛆⋧es膀\xd7;bd㛏㛐㛘䃗Ā;aᤏ㛕r;樱;樰ƀeps㛡㛣㜀\xe1⩍Ȁ;bcf҆㛬㛰㛴ot;挶ir;櫱Ā;o㛹㛼쀀\ud835\udd65rk;櫚\xe1㍢rime;怴ƀaip㜏㜒㝤d\xe5ቈ΀adempst㜡㝍㝀㝑㝗㝜㝟ngleʀ;dlqr㜰㜱㜶㝀㝂斵own\xbbᶻeftĀ;e⠀㜾\xf1म;扜ightĀ;e㊪㝋\xf1ၚot;旬inus;樺lus;樹b;槍ime;樻ezium;揢ƀcht㝲㝽㞁Āry㝷㝻;쀀\ud835\udcc9;䑆cy;䑛rok;䅧Āio㞋㞎x\xf4᝷headĀlr㞗㞠eftarro\xf7ࡏightarrow\xbbཝऀAHabcdfghlmoprstuw㟐㟓㟗㟤㟰㟼㠎㠜㠣㠴㡑㡝㡫㢩㣌㣒㣪㣶r\xf2ϭar;楣Ācr㟜㟢ute耻\xfa䃺\xf2ᅐrǣ㟪\0㟭y;䑞ve;䅭Āiy㟵㟺rc耻\xfb䃻;䑃ƀabh㠃㠆㠋r\xf2Ꭽlac;䅱a\xf2ᏃĀir㠓㠘sht;楾;쀀\ud835\udd32rave耻\xf9䃹š㠧㠱rĀlr㠬㠮\xbbॗ\xbbႃlk;斀Āct㠹㡍ɯ㠿\0\0㡊rnĀ;e㡅㡆挜r\xbb㡆op;挏ri;旸Āal㡖㡚cr;䅫肻\xa8͉Āgp㡢㡦on;䅳f;쀀\ud835\udd66̀adhlsuᅋ㡸㡽፲㢑㢠own\xe1ᎳarpoonĀlr㢈㢌ef\xf4㠭igh\xf4㠯iƀ;hl㢙㢚㢜䏅\xbbᏺon\xbb㢚parrows;懈ƀcit㢰㣄㣈ɯ㢶\0\0㣁rnĀ;e㢼㢽挝r\xbb㢽op;挎ng;䅯ri;旹cr;쀀\ud835\udccaƀdir㣙㣝㣢ot;拰lde;䅩iĀ;f㜰㣨\xbb᠓Āam㣯㣲r\xf2㢨l耻\xfc䃼angle;榧ހABDacdeflnoprsz㤜㤟㤩㤭㦵㦸㦽㧟㧤㧨㧳㧹㧽㨁㨠r\xf2ϷarĀ;v㤦㤧櫨;櫩as\xe8ϡĀnr㤲㤷grt;榜΀eknprst㓣㥆㥋㥒㥝㥤㦖app\xe1␕othin\xe7ẖƀhir㓫⻈㥙op\xf4⾵Ā;hᎷ㥢\xefㆍĀiu㥩㥭gm\xe1㎳Ābp㥲㦄setneqĀ;q㥽㦀쀀⊊︀;쀀⫋︀setneqĀ;q㦏㦒쀀⊋︀;쀀⫌︀Āhr㦛㦟et\xe1㚜iangleĀlr㦪㦯eft\xbbथight\xbbၑy;䐲ash\xbbံƀelr㧄㧒㧗ƀ;beⷪ㧋㧏ar;抻q;扚lip;拮Ābt㧜ᑨa\xf2ᑩr;쀀\ud835\udd33tr\xe9㦮suĀbp㧯㧱\xbbജ\xbb൙pf;쀀\ud835\udd67ro\xf0໻tr\xe9㦴Ācu㨆㨋r;쀀\ud835\udccbĀbp㨐㨘nĀEe㦀㨖\xbb㥾nĀEe㦒㨞\xbb㦐igzag;榚΀cefoprs㨶㨻㩖㩛㩔㩡㩪irc;䅵Ādi㩀㩑Ābg㩅㩉ar;機eĀ;qᗺ㩏;扙erp;愘r;쀀\ud835\udd34pf;쀀\ud835\udd68Ā;eᑹ㩦at\xe8ᑹcr;쀀\ud835\udcccૣណ㪇\0㪋\0㪐㪛\0\0㪝㪨㪫㪯\0\0㫃㫎\0㫘ៜ៟tr\xe9៑r;쀀\ud835\udd35ĀAa㪔㪗r\xf2σr\xf2৶;䎾ĀAa㪡㪤r\xf2θr\xf2৫a\xf0✓is;拻ƀdptឤ㪵㪾Āfl㪺ឩ;쀀\ud835\udd69im\xe5ឲĀAa㫇㫊r\xf2ώr\xf2ਁĀcq㫒ីr;쀀\ud835\udccdĀpt៖㫜r\xe9។Ѐacefiosu㫰㫽㬈㬌㬑㬕㬛㬡cĀuy㫶㫻te耻\xfd䃽;䑏Āiy㬂㬆rc;䅷;䑋n耻\xa5䂥r;쀀\ud835\udd36cy;䑗pf;쀀\ud835\udd6acr;쀀\ud835\udcceĀcm㬦㬩y;䑎l耻\xff䃿Ԁacdefhiosw㭂㭈㭔㭘㭤㭩㭭㭴㭺㮀cute;䅺Āay㭍㭒ron;䅾;䐷ot;䅼Āet㭝㭡tr\xe6ᕟa;䎶r;쀀\ud835\udd37cy;䐶grarr;懝pf;쀀\ud835\udd6bcr;쀀\ud835\udccfĀjn㮅㮇;怍j;怌' .split("") .map((e) => e.charCodeAt(0)) ); }, 5949: function (e, t, r) { r.d(t, { s: () => n }); let n = new Uint16Array( "Ȁaglq \x15\x18\x1bɭ\x0f\0\0\x12p;䀦os;䀧t;䀾t;䀼uot;䀢" .split("") .map((e) => e.charCodeAt(0)) ); }, 9496: function () {}, 8466: function (e, t, r) { r.d(t, { Gj: () => a.Gj, WY: () => a.WY, X1: () => a.X1 }), r(2990), r(466); var n, i, s, o, a = r(747); ((n = s || (s = {}))[(n.XML = 0)] = "XML"), (n[(n.HTML = 1)] = "HTML"), ((i = o || (o = {}))[(i.UTF8 = 0)] = "UTF8"), (i[(i.ASCII = 1)] = "ASCII"), (i[(i.Extensive = 2)] = "Extensive"), (i[(i.Attribute = 3)] = "Attribute"), (i[(i.Text = 4)] = "Text"); }, 4645: function (e, t, r) { r.d(t, { i: () => g }); var n = r(5645), i = r(2990); let s = new Set([ "input", "option", "optgroup", "select", "button", "datalist", "textarea", ]), o = new Set(["p"]), a = new Set(["thead", "tbody"]), l = new Set(["dd", "dt"]), c = new Set(["rt", "rp"]), u = new Map([ ["tr", new Set(["tr", "th", "td"])], ["th", new Set(["th"])], ["td", new Set(["thead", "th", "td"])], ["body", new Set(["head", "link", "script"])], ["li", new Set(["li"])], ["p", o], ["h1", o], ["h2", o], ["h3", o], ["h4", o], ["h5", o], ["h6", o], ["select", s], ["input", s], ["output", s], ["button", s], ["datalist", s], ["textarea", s], ["option", new Set(["option"])], ["optgroup", new Set(["optgroup", "option"])], ["dd", l], ["dt", l], ["address", o], ["article", o], ["aside", o], ["blockquote", o], ["details", o], ["div", o], ["dl", o], ["fieldset", o], ["figcaption", o], ["figure", o], ["footer", o], ["form", o], ["header", o], ["hr", o], ["main", o], ["nav", o], ["ol", o], ["pre", o], ["section", o], ["table", o], ["ul", o], ["rt", c], ["rp", c], ["tbody", a], ["tfoot", a], ]), d = new Set([ "area", "base", "basefont", "br", "col", "command", "embed", "frame", "hr", "img", "input", "isindex", "keygen", "link", "meta", "param", "source", "track", "wbr", ]), h = new Set(["math", "svg"]), p = new Set([ "mi", "mo", "mn", "ms", "mtext", "annotation-xml", "foreignobject", "desc", "title", ]), f = /\s|\//; class g { constructor(e, t = {}) { var r, i, s, o, a, l; (this.options = t), (this.startIndex = 0), (this.endIndex = 0), (this.openTagStart = 0), (this.tagname = ""), (this.attribname = ""), (this.attribvalue = ""), (this.attribs = null), (this.stack = []), (this.buffers = []), (this.bufferOffset = 0), (this.writeIndex = 0), (this.ended = !1), (this.cbs = null != e ? e : {}), (this.htmlMode = !this.options.xmlMode), (this.lowerCaseTagNames = null != (r = t.lowerCaseTags) ? r : this.htmlMode), (this.lowerCaseAttributeNames = null != (i = t.lowerCaseAttributeNames) ? i : this.htmlMode), (this.recognizeSelfClosing = null != (s = t.recognizeSelfClosing) ? s : !this.htmlMode), (this.tokenizer = new (null != (o = t.Tokenizer) ? o : n.A)( this.options, this )), (this.foreignContext = [!this.htmlMode]), null == (l = (a = this.cbs).onparserinit) || l.call(a, this); } ontext(e, t) { var r, n; let i = this.getSlice(e, t); (this.endIndex = t - 1), null == (n = (r = this.cbs).ontext) || n.call(r, i), (this.startIndex = t); } ontextentity(e, t) { var r, n; (this.endIndex = t - 1), null == (n = (r = this.cbs).ontext) || n.call(r, (0, i.MK)(e)), (this.startIndex = t); } isVoidElement(e) { return this.htmlMode && d.has(e); } onopentagname(e, t) { this.endIndex = t; let r = this.getSlice(e, t); this.lowerCaseTagNames && (r = r.toLowerCase()), this.emitOpenTag(r); } emitOpenTag(e) { var t, r, n, i; (this.openTagStart = this.startIndex), (this.tagname = e); let s = this.htmlMode && u.get(e); if (s) for (; this.stack.length > 0 && s.has(this.stack[0]); ) { let e = this.stack.shift(); null == (r = (t = this.cbs).onclosetag) || r.call(t, e, !0); } !this.isVoidElement(e) && (this.stack.unshift(e), this.htmlMode && (h.has(e) ? this.foreignContext.unshift(!0) : p.has(e) && this.foreignContext.unshift(!1))), null == (i = (n = this.cbs).onopentagname) || i.call(n, e), this.cbs.onopentag && (this.attribs = {}); } endOpenTag(e) { var t, r; (this.startIndex = this.openTagStart), this.attribs && (null == (r = (t = this.cbs).onopentag) || r.call(t, this.tagname, this.attribs, e), (this.attribs = null)), this.cbs.onclosetag && this.isVoidElement(this.tagname) && this.cbs.onclosetag(this.tagname, !0), (this.tagname = ""); } onopentagend(e) { (this.endIndex = e), this.endOpenTag(!1), (this.startIndex = e + 1); } onclosetag(e, t) { var r, n, i, s, o, a, l, c; this.endIndex = t; let u = this.getSlice(e, t); if ( (this.lowerCaseTagNames && (u = u.toLowerCase()), this.htmlMode && (h.has(u) || p.has(u)) && this.foreignContext.shift(), this.isVoidElement(u)) ) this.htmlMode && "br" === u && (null == (s = (i = this.cbs).onopentagname) || s.call(i, "br"), null == (a = (o = this.cbs).onopentag) || a.call(o, "br", {}, !0), null == (c = (l = this.cbs).onclosetag) || c.call(l, "br", !1)); else { let e = this.stack.indexOf(u); if (-1 !== e) for (let t = 0; t <= e; t++) { let i = this.stack.shift(); null == (n = (r = this.cbs).onclosetag) || n.call(r, i, t !== e); } else this.htmlMode && "p" === u && (this.emitOpenTag("p"), this.closeCurrentTag(!0)); } this.startIndex = t + 1; } onselfclosingtag(e) { (this.endIndex = e), this.recognizeSelfClosing || this.foreignContext[0] ? (this.closeCurrentTag(!1), (this.startIndex = e + 1)) : this.onopentagend(e); } closeCurrentTag(e) { var t, r; let n = this.tagname; this.endOpenTag(e), this.stack[0] === n && (null == (r = (t = this.cbs).onclosetag) || r.call(t, n, !e), this.stack.shift()); } onattribname(e, t) { this.startIndex = e; let r = this.getSlice(e, t); this.attribname = this.lowerCaseAttributeNames ? r.toLowerCase() : r; } onattribdata(e, t) { this.attribvalue += this.getSlice(e, t); } onattribentity(e) { this.attribvalue += (0, i.MK)(e); } onattribend(e, t) { var r, i; (this.endIndex = t), null == (i = (r = this.cbs).onattribute) || i.call( r, this.attribname, this.attribvalue, e === n.X.Double ? '"' : e === n.X.Single ? "'" : e === n.X.NoValue ? void 0 : null ), this.attribs && !Object.prototype.hasOwnProperty.call( this.attribs, this.attribname ) && (this.attribs[this.attribname] = this.attribvalue), (this.attribvalue = ""); } getInstructionName(e) { let t = e.search(f), r = t < 0 ? e : e.substr(0, t); return this.lowerCaseTagNames && (r = r.toLowerCase()), r; } ondeclaration(e, t) { this.endIndex = t; let r = this.getSlice(e, t); if (this.cbs.onprocessinginstruction) { let e = this.getInstructionName(r); this.cbs.onprocessinginstruction(`!${e}`, `!${r}`); } this.startIndex = t + 1; } onprocessinginstruction(e, t) { this.endIndex = t; let r = this.getSlice(e, t); if (this.cbs.onprocessinginstruction) { let e = this.getInstructionName(r); this.cbs.onprocessinginstruction(`?${e}`, `?${r}`); } this.startIndex = t + 1; } oncomment(e, t, r) { var n, i, s, o; (this.endIndex = t), null == (i = (n = this.cbs).oncomment) || i.call(n, this.getSlice(e, t - r)), null == (o = (s = this.cbs).oncommentend) || o.call(s), (this.startIndex = t + 1); } oncdata(e, t, r) { var n, i, s, o, a, l, c, u, d, h; this.endIndex = t; let p = this.getSlice(e, t - r); !this.htmlMode || this.options.recognizeCDATA ? (null == (i = (n = this.cbs).oncdatastart) || i.call(n), null == (o = (s = this.cbs).ontext) || o.call(s, p), null == (l = (a = this.cbs).oncdataend) || l.call(a)) : (null == (u = (c = this.cbs).oncomment) || u.call(c, `[CDATA[${p}]]`), null == (h = (d = this.cbs).oncommentend) || h.call(d)), (this.startIndex = t + 1); } onend() { var e, t; if (this.cbs.onclosetag) { this.endIndex = this.startIndex; for (let e = 0; e < this.stack.length; e++) this.cbs.onclosetag(this.stack[e], !0); } null == (t = (e = this.cbs).onend) || t.call(e); } reset() { var e, t, r, n; null == (t = (e = this.cbs).onreset) || t.call(e), this.tokenizer.reset(), (this.tagname = ""), (this.attribname = ""), (this.attribs = null), (this.stack.length = 0), (this.startIndex = 0), (this.endIndex = 0), null == (n = (r = this.cbs).onparserinit) || n.call(r, this), (this.buffers.length = 0), (this.foreignContext.length = 0), this.foreignContext.unshift(!this.htmlMode), (this.bufferOffset = 0), (this.writeIndex = 0), (this.ended = !1); } parseComplete(e) { this.reset(), this.end(e); } getSlice(e, t) { for (; e - this.bufferOffset >= this.buffers[0].length; ) this.shiftBuffer(); let r = this.buffers[0].slice( e - this.bufferOffset, t - this.bufferOffset ); for (; t - this.bufferOffset > this.buffers[0].length; ) this.shiftBuffer(), (r += this.buffers[0].slice(0, t - this.bufferOffset)); return r; } shiftBuffer() { (this.bufferOffset += this.buffers[0].length), this.writeIndex--, this.buffers.shift(); } write(e) { var t, r; if (this.ended) { null == (r = (t = this.cbs).onerror) || r.call(t, Error(".write() after done!")); return; } this.buffers.push(e), this.tokenizer.running && (this.tokenizer.write(e), this.writeIndex++); } end(e) { var t, r; if (this.ended) { null == (r = (t = this.cbs).onerror) || r.call(t, Error(".end() after done!")); return; } e && this.write(e), (this.ended = !0), this.tokenizer.end(); } pause() { this.tokenizer.pause(); } resume() { for ( this.tokenizer.resume(); this.tokenizer.running && this.writeIndex < this.buffers.length; ) this.tokenizer.write(this.buffers[this.writeIndex++]); this.ended && this.tokenizer.end(); } parseChunk(e) { this.write(e); } done(e) { this.end(e); } } }, 5645: function (e, t, r) { r.d(t, { A: () => p, X: () => l }); var n, i, s, o, a, l, c = r(2990); function u(e) { return ( e === o.Space || e === o.NewLine || e === o.Tab || e === o.FormFeed || e === o.CarriageReturn ); } function d(e) { return e === o.Slash || e === o.Gt || u(e); } ((n = o || (o = {}))[(n.Tab = 9)] = "Tab"), (n[(n.NewLine = 10)] = "NewLine"), (n[(n.FormFeed = 12)] = "FormFeed"), (n[(n.CarriageReturn = 13)] = "CarriageReturn"), (n[(n.Space = 32)] = "Space"), (n[(n.ExclamationMark = 33)] = "ExclamationMark"), (n[(n.Number = 35)] = "Number"), (n[(n.Amp = 38)] = "Amp"), (n[(n.SingleQuote = 39)] = "SingleQuote"), (n[(n.DoubleQuote = 34)] = "DoubleQuote"), (n[(n.Dash = 45)] = "Dash"), (n[(n.Slash = 47)] = "Slash"), (n[(n.Zero = 48)] = "Zero"), (n[(n.Nine = 57)] = "Nine"), (n[(n.Semi = 59)] = "Semi"), (n[(n.Lt = 60)] = "Lt"), (n[(n.Eq = 61)] = "Eq"), (n[(n.Gt = 62)] = "Gt"), (n[(n.Questionmark = 63)] = "Questionmark"), (n[(n.UpperA = 65)] = "UpperA"), (n[(n.LowerA = 97)] = "LowerA"), (n[(n.UpperF = 70)] = "UpperF"), (n[(n.LowerF = 102)] = "LowerF"), (n[(n.UpperZ = 90)] = "UpperZ"), (n[(n.LowerZ = 122)] = "LowerZ"), (n[(n.LowerX = 120)] = "LowerX"), (n[(n.OpeningSquareBracket = 91)] = "OpeningSquareBracket"), ((i = a || (a = {}))[(i.Text = 1)] = "Text"), (i[(i.BeforeTagName = 2)] = "BeforeTagName"), (i[(i.InTagName = 3)] = "InTagName"), (i[(i.InSelfClosingTag = 4)] = "InSelfClosingTag"), (i[(i.BeforeClosingTagName = 5)] = "BeforeClosingTagName"), (i[(i.InClosingTagName = 6)] = "InClosingTagName"), (i[(i.AfterClosingTagName = 7)] = "AfterClosingTagName"), (i[(i.BeforeAttributeName = 8)] = "BeforeAttributeName"), (i[(i.InAttributeName = 9)] = "InAttributeName"), (i[(i.AfterAttributeName = 10)] = "AfterAttributeName"), (i[(i.BeforeAttributeValue = 11)] = "BeforeAttributeValue"), (i[(i.InAttributeValueDq = 12)] = "InAttributeValueDq"), (i[(i.InAttributeValueSq = 13)] = "InAttributeValueSq"), (i[(i.InAttributeValueNq = 14)] = "InAttributeValueNq"), (i[(i.BeforeDeclaration = 15)] = "BeforeDeclaration"), (i[(i.InDeclaration = 16)] = "InDeclaration"), (i[(i.InProcessingInstruction = 17)] = "InProcessingInstruction"), (i[(i.BeforeComment = 18)] = "BeforeComment"), (i[(i.CDATASequence = 19)] = "CDATASequence"), (i[(i.InSpecialComment = 20)] = "InSpecialComment"), (i[(i.InCommentLike = 21)] = "InCommentLike"), (i[(i.BeforeSpecialS = 22)] = "BeforeSpecialS"), (i[(i.BeforeSpecialT = 23)] = "BeforeSpecialT"), (i[(i.SpecialStartSequence = 24)] = "SpecialStartSequence"), (i[(i.InSpecialTag = 25)] = "InSpecialTag"), (i[(i.InEntity = 26)] = "InEntity"), ((s = l || (l = {}))[(s.NoValue = 0)] = "NoValue"), (s[(s.Unquoted = 1)] = "Unquoted"), (s[(s.Single = 2)] = "Single"), (s[(s.Double = 3)] = "Double"); let h = { Cdata: new Uint8Array([67, 68, 65, 84, 65, 91]), CdataEnd: new Uint8Array([93, 93, 62]), CommentEnd: new Uint8Array([45, 45, 62]), ScriptEnd: new Uint8Array([60, 47, 115, 99, 114, 105, 112, 116]), StyleEnd: new Uint8Array([60, 47, 115, 116, 121, 108, 101]), TitleEnd: new Uint8Array([60, 47, 116, 105, 116, 108, 101]), TextareaEnd: new Uint8Array([ 60, 47, 116, 101, 120, 116, 97, 114, 101, 97, ]), XmpEnd: new Uint8Array([60, 47, 120, 109, 112]), }; class p { constructor({ xmlMode: e = !1, decodeEntities: t = !0 }, r) { (this.cbs = r), (this.state = a.Text), (this.buffer = ""), (this.sectionStart = 0), (this.index = 0), (this.entityStart = 0), (this.baseState = a.Text), (this.isSpecial = !1), (this.running = !0), (this.offset = 0), (this.currentSequence = void 0), (this.sequenceIndex = 0), (this.xmlMode = e), (this.decodeEntities = t), (this.entityDecoder = new c.Wf(e ? c.sr : c.qN, (e, t) => this.emitCodePoint(e, t) )); } reset() { (this.state = a.Text), (this.buffer = ""), (this.sectionStart = 0), (this.index = 0), (this.baseState = a.Text), (this.currentSequence = void 0), (this.running = !0), (this.offset = 0); } write(e) { (this.offset += this.buffer.length), (this.buffer = e), this.parse(); } end() { this.running && this.finish(); } pause() { this.running = !1; } resume() { (this.running = !0), this.index < this.buffer.length + this.offset && this.parse(); } stateText(e) { e === o.Lt || (!this.decodeEntities && this.fastForwardTo(o.Lt)) ? (this.index > this.sectionStart && this.cbs.ontext(this.sectionStart, this.index), (this.state = a.BeforeTagName), (this.sectionStart = this.index)) : this.decodeEntities && e === o.Amp && this.startEntity(); } stateSpecialStartSequence(e) { let t = this.sequenceIndex === this.currentSequence.length; if ( t ? d(e) : (32 | e) === this.currentSequence[this.sequenceIndex] ) { if (!t) return void this.sequenceIndex++; } else this.isSpecial = !1; (this.sequenceIndex = 0), (this.state = a.InTagName), this.stateInTagName(e); } stateInSpecialTag(e) { if (this.sequenceIndex === this.currentSequence.length) { if (e === o.Gt || u(e)) { let t = this.index - this.currentSequence.length; if (this.sectionStart < t) { let e = this.index; (this.index = t), this.cbs.ontext(this.sectionStart, t), (this.index = e); } (this.isSpecial = !1), (this.sectionStart = t + 2), this.stateInClosingTagName(e); return; } this.sequenceIndex = 0; } (32 | e) === this.currentSequence[this.sequenceIndex] ? (this.sequenceIndex += 1) : 0 === this.sequenceIndex ? this.currentSequence === h.TitleEnd ? this.decodeEntities && e === o.Amp && this.startEntity() : this.fastForwardTo(o.Lt) && (this.sequenceIndex = 1) : (this.sequenceIndex = Number(e === o.Lt)); } stateCDATASequence(e) { e === h.Cdata[this.sequenceIndex] ? ++this.sequenceIndex === h.Cdata.length && ((this.state = a.InCommentLike), (this.currentSequence = h.CdataEnd), (this.sequenceIndex = 0), (this.sectionStart = this.index + 1)) : ((this.sequenceIndex = 0), (this.state = a.InDeclaration), this.stateInDeclaration(e)); } fastForwardTo(e) { for (; ++this.index < this.buffer.length + this.offset; ) if (this.buffer.charCodeAt(this.index - this.offset) === e) return !0; return (this.index = this.buffer.length + this.offset - 1), !1; } stateInCommentLike(e) { e === this.currentSequence[this.sequenceIndex] ? ++this.sequenceIndex === this.currentSequence.length && (this.currentSequence === h.CdataEnd ? this.cbs.oncdata(this.sectionStart, this.index, 2) : this.cbs.oncomment(this.sectionStart, this.index, 2), (this.sequenceIndex = 0), (this.sectionStart = this.index + 1), (this.state = a.Text)) : 0 === this.sequenceIndex ? this.fastForwardTo(this.currentSequence[0]) && (this.sequenceIndex = 1) : e !== this.currentSequence[this.sequenceIndex - 1] && (this.sequenceIndex = 0); } isTagStartChar(e) { return this.xmlMode ? !d(e) : (e >= o.LowerA && e <= o.LowerZ) || (e >= o.UpperA && e <= o.UpperZ); } startSpecial(e, t) { (this.isSpecial = !0), (this.currentSequence = e), (this.sequenceIndex = t), (this.state = a.SpecialStartSequence); } stateBeforeTagName(e) { if (e === o.ExclamationMark) (this.state = a.BeforeDeclaration), (this.sectionStart = this.index + 1); else if (e === o.Questionmark) (this.state = a.InProcessingInstruction), (this.sectionStart = this.index + 1); else if (this.isTagStartChar(e)) { let t = 32 | e; (this.sectionStart = this.index), this.xmlMode ? (this.state = a.InTagName) : t === h.ScriptEnd[2] ? (this.state = a.BeforeSpecialS) : t === h.TitleEnd[2] || t === h.XmpEnd[2] ? (this.state = a.BeforeSpecialT) : (this.state = a.InTagName); } else e === o.Slash ? (this.state = a.BeforeClosingTagName) : ((this.state = a.Text), this.stateText(e)); } stateInTagName(e) { d(e) && (this.cbs.onopentagname(this.sectionStart, this.index), (this.sectionStart = -1), (this.state = a.BeforeAttributeName), this.stateBeforeAttributeName(e)); } stateBeforeClosingTagName(e) { u(e) || (e === o.Gt ? (this.state = a.Text) : ((this.state = this.isTagStartChar(e) ? a.InClosingTagName : a.InSpecialComment), (this.sectionStart = this.index))); } stateInClosingTagName(e) { (e === o.Gt || u(e)) && (this.cbs.onclosetag(this.sectionStart, this.index), (this.sectionStart = -1), (this.state = a.AfterClosingTagName), this.stateAfterClosingTagName(e)); } stateAfterClosingTagName(e) { (e === o.Gt || this.fastForwardTo(o.Gt)) && ((this.state = a.Text), (this.sectionStart = this.index + 1)); } stateBeforeAttributeName(e) { e === o.Gt ? (this.cbs.onopentagend(this.index), this.isSpecial ? ((this.state = a.InSpecialTag), (this.sequenceIndex = 0)) : (this.state = a.Text), (this.sectionStart = this.index + 1)) : e === o.Slash ? (this.state = a.InSelfClosingTag) : u(e) || ((this.state = a.InAttributeName), (this.sectionStart = this.index)); } stateInSelfClosingTag(e) { e === o.Gt ? (this.cbs.onselfclosingtag(this.index), (this.state = a.Text), (this.sectionStart = this.index + 1), (this.isSpecial = !1)) : u(e) || ((this.state = a.BeforeAttributeName), this.stateBeforeAttributeName(e)); } stateInAttributeName(e) { (e === o.Eq || d(e)) && (this.cbs.onattribname(this.sectionStart, this.index), (this.sectionStart = this.index), (this.state = a.AfterAttributeName), this.stateAfterAttributeName(e)); } stateAfterAttributeName(e) { e === o.Eq ? (this.state = a.BeforeAttributeValue) : e === o.Slash || e === o.Gt ? (this.cbs.onattribend(l.NoValue, this.sectionStart), (this.sectionStart = -1), (this.state = a.BeforeAttributeName), this.stateBeforeAttributeName(e)) : u(e) || (this.cbs.onattribend(l.NoValue, this.sectionStart), (this.state = a.InAttributeName), (this.sectionStart = this.index)); } stateBeforeAttributeValue(e) { e === o.DoubleQuote ? ((this.state = a.InAttributeValueDq), (this.sectionStart = this.index + 1)) : e === o.SingleQuote ? ((this.state = a.InAttributeValueSq), (this.sectionStart = this.index + 1)) : u(e) || ((this.sectionStart = this.index), (this.state = a.InAttributeValueNq), this.stateInAttributeValueNoQuotes(e)); } handleInAttributeValue(e, t) { e === t || (!this.decodeEntities && this.fastForwardTo(t)) ? (this.cbs.onattribdata(this.sectionStart, this.index), (this.sectionStart = -1), this.cbs.onattribend( t === o.DoubleQuote ? l.Double : l.Single, this.index + 1 ), (this.state = a.BeforeAttributeName)) : this.decodeEntities && e === o.Amp && this.startEntity(); } stateInAttributeValueDoubleQuotes(e) { this.handleInAttributeValue(e, o.DoubleQuote); } stateInAttributeValueSingleQuotes(e) { this.handleInAttributeValue(e, o.SingleQuote); } stateInAttributeValueNoQuotes(e) { u(e) || e === o.Gt ? (this.cbs.onattribdata(this.sectionStart, this.index), (this.sectionStart = -1), this.cbs.onattribend(l.Unquoted, this.index), (this.state = a.BeforeAttributeName), this.stateBeforeAttributeName(e)) : this.decodeEntities && e === o.Amp && this.startEntity(); } stateBeforeDeclaration(e) { e === o.OpeningSquareBracket ? ((this.state = a.CDATASequence), (this.sequenceIndex = 0)) : (this.state = e === o.Dash ? a.BeforeComment : a.InDeclaration); } stateInDeclaration(e) { (e === o.Gt || this.fastForwardTo(o.Gt)) && (this.cbs.ondeclaration(this.sectionStart, this.index), (this.state = a.Text), (this.sectionStart = this.index + 1)); } stateInProcessingInstruction(e) { (e === o.Gt || this.fastForwardTo(o.Gt)) && (this.cbs.onprocessinginstruction(this.sectionStart, this.index), (this.state = a.Text), (this.sectionStart = this.index + 1)); } stateBeforeComment(e) { e === o.Dash ? ((this.state = a.InCommentLike), (this.currentSequence = h.CommentEnd), (this.sequenceIndex = 2), (this.sectionStart = this.index + 1)) : (this.state = a.InDeclaration); } stateInSpecialComment(e) { (e === o.Gt || this.fastForwardTo(o.Gt)) && (this.cbs.oncomment(this.sectionStart, this.index, 0), (this.state = a.Text), (this.sectionStart = this.index + 1)); } stateBeforeSpecialS(e) { let t = 32 | e; t === h.ScriptEnd[3] ? this.startSpecial(h.ScriptEnd, 4) : t === h.StyleEnd[3] ? this.startSpecial(h.StyleEnd, 4) : ((this.state = a.InTagName), this.stateInTagName(e)); } stateBeforeSpecialT(e) { switch (32 | e) { case h.TitleEnd[3]: this.startSpecial(h.TitleEnd, 4); break; case h.TextareaEnd[3]: this.startSpecial(h.TextareaEnd, 4); break; case h.XmpEnd[3]: this.startSpecial(h.XmpEnd, 4); break; default: (this.state = a.InTagName), this.stateInTagName(e); } } startEntity() { (this.baseState = this.state), (this.state = a.InEntity), (this.entityStart = this.index), this.entityDecoder.startEntity( this.xmlMode ? c.FJ.Strict : this.baseState === a.Text || this.baseState === a.InSpecialTag ? c.FJ.Legacy : c.FJ.Attribute ); } stateInEntity() { let e = this.entityDecoder.write( this.buffer, this.index - this.offset ); e >= 0 ? ((this.state = this.baseState), 0 === e && (this.index = this.entityStart)) : (this.index = this.offset + this.buffer.length - 1); } cleanup() { this.running && this.sectionStart !== this.index && (this.state === a.Text || (this.state === a.InSpecialTag && 0 === this.sequenceIndex) ? (this.cbs.ontext(this.sectionStart, this.index), (this.sectionStart = this.index)) : (this.state === a.InAttributeValueDq || this.state === a.InAttributeValueSq || this.state === a.InAttributeValueNq) && (this.cbs.onattribdata(this.sectionStart, this.index), (this.sectionStart = this.index))); } shouldContinue() { return ( this.index < this.buffer.length + this.offset && this.running ); } parse() { for (; this.shouldContinue(); ) { let e = this.buffer.charCodeAt(this.index - this.offset); switch (this.state) { case a.Text: this.stateText(e); break; case a.SpecialStartSequence: this.stateSpecialStartSequence(e); break; case a.InSpecialTag: this.stateInSpecialTag(e); break; case a.CDATASequence: this.stateCDATASequence(e); break; case a.InAttributeValueDq: this.stateInAttributeValueDoubleQuotes(e); break; case a.InAttributeName: this.stateInAttributeName(e); break; case a.InCommentLike: this.stateInCommentLike(e); break; case a.InSpecialComment: this.stateInSpecialComment(e); break; case a.BeforeAttributeName: this.stateBeforeAttributeName(e); break; case a.InTagName: this.stateInTagName(e); break; case a.InClosingTagName: this.stateInClosingTagName(e); break; case a.BeforeTagName: this.stateBeforeTagName(e); break; case a.AfterAttributeName: this.stateAfterAttributeName(e); break; case a.InAttributeValueSq: this.stateInAttributeValueSingleQuotes(e); break; case a.BeforeAttributeValue: this.stateBeforeAttributeValue(e); break; case a.BeforeClosingTagName: this.stateBeforeClosingTagName(e); break; case a.AfterClosingTagName: this.stateAfterClosingTagName(e); break; case a.BeforeSpecialS: this.stateBeforeSpecialS(e); break; case a.BeforeSpecialT: this.stateBeforeSpecialT(e); break; case a.InAttributeValueNq: this.stateInAttributeValueNoQuotes(e); break; case a.InSelfClosingTag: this.stateInSelfClosingTag(e); break; case a.InDeclaration: this.stateInDeclaration(e); break; case a.BeforeDeclaration: this.stateBeforeDeclaration(e); break; case a.BeforeComment: this.stateBeforeComment(e); break; case a.InProcessingInstruction: this.stateInProcessingInstruction(e); break; case a.InEntity: this.stateInEntity(); } this.index++; } this.cleanup(); } finish() { this.state === a.InEntity && (this.entityDecoder.end(), (this.state = this.baseState)), this.handleTrailingData(), this.cbs.onend(); } handleTrailingData() { let e = this.buffer.length + this.offset; this.sectionStart >= e || (this.state === a.InCommentLike ? this.currentSequence === h.CdataEnd ? this.cbs.oncdata(this.sectionStart, e, 0) : this.cbs.oncomment(this.sectionStart, e, 0) : this.state === a.InTagName || this.state === a.BeforeAttributeName || this.state === a.BeforeAttributeValue || this.state === a.AfterAttributeName || this.state === a.InAttributeName || this.state === a.InAttributeValueSq || this.state === a.InAttributeValueDq || this.state === a.InAttributeValueNq || this.state === a.InClosingTagName || this.cbs.ontext(this.sectionStart, e)); } emitCodePoint(e, t) { this.baseState !== a.Text && this.baseState !== a.InSpecialTag ? (this.sectionStart < this.entityStart && this.cbs.onattribdata(this.sectionStart, this.entityStart), (this.sectionStart = this.entityStart + t), (this.index = this.sectionStart - 1), this.cbs.onattribentity(e)) : (this.sectionStart < this.entityStart && this.cbs.ontext(this.sectionStart, this.entityStart), (this.sectionStart = this.entityStart + t), (this.index = this.sectionStart - 1), this.cbs.ontextentity(e, this.sectionStart)); } } }, 3808: function (e, t, r) { r.d(t, { RJ: () => i, iX: () => n.i }); var n = r(4645); r(8866), r(5645); var i = r(2743); r(4993); }, 1652: function (e, t, r) { r.d(t, { N: () => n }); function n() { return "10000000000".replace(/[018]/g, (e) => ( e ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (e / 4))) ).toString(16) ); } }, 3907: function (e, t, r) { let n; r.d(t, { LW: () => b, QR: () => v }); var i = r(1652); function s(e, t) { try { return e.apply(this, t); } catch (t) { let e = (function (e) { let t = n.__externref_table_alloc(); return n.__wbindgen_export_2.set(t, e), t; })(t); n.__wbindgen_exn_store(e); } } let o = "undefined" != typeof TextDecoder ? new TextDecoder("utf-8", { ignoreBOM: !0, fatal: !0 }) : { decode: () => { throw Error("TextDecoder not available"); }, }; "undefined" != typeof TextDecoder && o.decode(); let a = null; function l() { return ( (null === a || 0 === a.byteLength) && (a = new Uint8Array(n.memory.buffer)), a ); } function c(e, t) { return (e >>>= 0), o.decode(l().subarray(e, e + t)); } let u = 0, d = "undefined" != typeof TextEncoder ? new TextEncoder("utf-8") : { encode: () => { throw Error("TextEncoder not available"); }, }, h = "function" == typeof d.encodeInto ? function (e, t) { return d.encodeInto(e, t); } : function (e, t) { let r = d.encode(e); return t.set(r), { read: e.length, written: r.length }; }; function p(e, t, r) { if (void 0 === r) { let r = d.encode(e), n = t(r.length, 1) >>> 0; return ( l() .subarray(n, n + r.length) .set(r), (u = r.length), n ); } let n = e.length, i = t(n, 1) >>> 0, s = l(), o = 0; for (; o < n; o++) { let t = e.charCodeAt(o); if (t > 127) break; s[i + o] = t; } if (o !== n) { 0 !== o && (e = e.slice(o)), (i = r(i, n, (n = o + 3 * e.length), 1) >>> 0); let t = h(e, l().subarray(i + o, i + n)); (o += t.written), (i = r(i, n, o, 1) >>> 0); } return (u = o), i; } let f = null; function g() { return ( (null === f || !0 === f.buffer.detached || (void 0 === f.buffer.detached && f.buffer !== n.memory.buffer)) && (f = new DataView(n.memory.buffer)), f ); } function m(e) { let t = n.__wbindgen_export_2.get(e); return n.__externref_table_dealloc(e), t; } let y = "undefined" == typeof FinalizationRegistry ? { register: () => {}, unregister: () => {} } : new FinalizationRegistry((e) => n.__wbg_rewriter_free(e >>> 0, 1) ); class b { __destroy_into_raw() { let e = this.__wbg_ptr; return (this.__wbg_ptr = 0), y.unregister(this), e; } free() { let e = this.__destroy_into_raw(); n.__wbg_rewriter_free(e, 0); } rewrite_js(e, t, r, i) { let s = p(e, n.__wbindgen_malloc, n.__wbindgen_realloc), o = u, a = p(t, n.__wbindgen_malloc, n.__wbindgen_realloc), l = u, c = p(r, n.__wbindgen_malloc, n.__wbindgen_realloc), d = u, h = n.rewriter_rewrite_js(this.__wbg_ptr, s, o, a, l, c, d, i); if (h[2]) throw m(h[1]); return m(h[0]); } rewrite_js_bytes(e, t, r, i) { let s = (function (e, t) { let r = t(+e.length, 1) >>> 0; return l().set(e, r / 1), (u = e.length), r; })(e, n.__wbindgen_malloc), o = u, a = p(t, n.__wbindgen_malloc, n.__wbindgen_realloc), c = u, d = p(r, n.__wbindgen_malloc, n.__wbindgen_realloc), h = u, f = n.rewriter_rewrite_js_bytes( this.__wbg_ptr, s, o, a, c, d, h, i ); if (f[2]) throw m(f[1]); return m(f[0]); } constructor(e) { let t = n.rewriter_new(e); if (t[2]) throw m(t[1]); return ( (this.__wbg_ptr = t[0] >>> 0), y.register(this, this.__wbg_ptr, this), this ); } } async function w(e, t) { if ("function" == typeof Response && e instanceof Response) { if ("function" == typeof WebAssembly.instantiateStreaming) try { return await WebAssembly.instantiateStreaming(e, t); } catch (t) { if ("application/wasm" != e.headers.get("Content-Type")) console.warn( "`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", t ); else throw t; } let r = await e.arrayBuffer(); return await WebAssembly.instantiate(r, t); } { let r = await WebAssembly.instantiate(e, t); return r instanceof WebAssembly.Instance ? { instance: r, module: e } : r; } } function S() { let e = {}; return ( (e.wbg = {}), (e.wbg.__wbg_buffer_609cc3eee51ed158 = function (e) { return e.buffer; }), (e.wbg.__wbg_call_7cccdd69e0791ae2 = function () { return s(function (e, t, r) { return e.call(t, r); }, arguments); }), (e.wbg.__wbg_call_833bed5770ea2041 = function () { return s(function (e, t, r, n) { return e.call(t, r, n); }, arguments); }), (e.wbg.__wbg_get_67b2ba62fc30de12 = function () { return s(function (e, t) { return Reflect.get(e, t); }, arguments); }), (e.wbg.__wbg_new_405e22f390576ce2 = function () { return {}; }), (e.wbg.__wbg_new_78feb108b6472713 = function () { return []; }), (e.wbg.__wbg_new_9ffbe0a71eff35e3 = function () { return s(function (e, t) { return new URL(c(e, t)); }, arguments); }), (e.wbg.__wbg_new_a12002a7f91c75be = function (e) { return new Uint8Array(e); }), (e.wbg.__wbg_newwithbase_161c299e7a34e2eb = function () { return s(function (e, t, r, n) { return new URL(c(e, t), c(r, n)); }, arguments); }), (e.wbg.__wbg_newwithbyteoffsetandlength_d97e637ebe145a9a = function (e, t, r) { return new Uint8Array(e, t >>> 0, r >>> 0); }), (e.wbg.__wbg_scramtag_3a255d78b157986d = function (e) { let t = p((0, i.N)(), n.__wbindgen_malloc, n.__wbindgen_realloc), r = u; g().setInt32(e + 4, r, !0), g().setInt32(e + 0, t, !0); }), (e.wbg.__wbg_set_bb8cecf6a62b9f46 = function () { return s(function (e, t, r) { return Reflect.set(e, t, r); }, arguments); }), (e.wbg.__wbg_toString_5285597960676b7b = function (e) { return e.toString(); }), (e.wbg.__wbg_toString_c813bbd34d063839 = function (e) { return e.toString(); }), (e.wbg.__wbindgen_boolean_get = function (e) { return "boolean" == typeof e ? +!!e : 2; }), (e.wbg.__wbindgen_error_new = function (e, t) { return Error(c(e, t)); }), (e.wbg.__wbindgen_init_externref_table = function () { let e = n.__wbindgen_export_2, t = e.grow(4); e.set(0, void 0), e.set(t + 0, void 0), e.set(t + 1, null), e.set(t + 2, !0), e.set(t + 3, !1); }), (e.wbg.__wbindgen_is_function = function (e) { return "function" == typeof e; }), (e.wbg.__wbindgen_memory = function () { return n.memory; }), (e.wbg.__wbindgen_string_get = function (e, t) { let r = "string" == typeof t ? t : void 0; var i = null == r ? 0 : p(r, n.__wbindgen_malloc, n.__wbindgen_realloc), s = u; g().setInt32(e + 4, s, !0), g().setInt32(e + 0, i, !0); }), (e.wbg.__wbindgen_string_new = function (e, t) { return c(e, t); }), (e.wbg.__wbindgen_throw = function (e, t) { throw Error(c(e, t)); }), e ); } function x(e, t) { return ( (n = e.exports), (E.__wbindgen_wasm_module = t), (f = null), (a = null), n.__wbindgen_start(), n ); } function v(e) { if (void 0 !== n) return n; void 0 !== e && (Object.getPrototypeOf(e) === Object.prototype ? ({ module: e } = e) : console.warn( "using deprecated parameters for `initSync()`; pass a single object instead" )); let t = S(); return ( e instanceof WebAssembly.Module || (e = new WebAssembly.Module(e)), x(new WebAssembly.Instance(e, t), e) ); } async function E(e) { if (void 0 !== n) return n; void 0 !== e && (Object.getPrototypeOf(e) === Object.prototype ? ({ module_or_path: e } = e) : console.warn( "using deprecated parameters for the initialization function; pass a single object instead" )), void 0 === e && (e = new URL("wasm_bg.wasm", "")); let t = S(); ("string" == typeof e || ("function" == typeof Request && e instanceof Request) || ("function" == typeof URL && e instanceof URL)) && (e = fetch(e)); let { instance: r, module: i } = await w(await e, t); return x(r, i); } }, }, t = {}; function r(n) { var i = t[n]; if (void 0 !== i) return i.exports; var s = (t[n] = { exports: {} }); return e[n](s, s.exports, r), s.exports; } (r.n = (e) => { var t = e && e.__esModule ? () => e.default : () => e; return r.d(t, { a: t }), t; }), (r.d = (e, t) => { for (var n in t) r.o(t, n) && !r.o(e, n) && Object.defineProperty(e, n, { enumerable: !0, get: t[n] }); }), (r.o = (e, t) => Object.prototype.hasOwnProperty.call(e, t)), (r.r = (e) => { "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, { value: "Module" }), Object.defineProperty(e, "__esModule", { value: !0 }); }), (globalThis.$scramjetRequire = function (e) { return r(409)(e); }), (globalThis.$scramjetLoadController = function () { return r(9052); }), (globalThis.$scramjetLoadClient = function () { return r(1323); }), (globalThis.$scramjetLoadWorker = function () { return r(7510); }), (globalThis.$scramjetVersion = { build: "947bc65", version: "2.0.0-alpha", }), "document" in globalThis && document?.currentScript && document.currentScript.remove(); })();

NEBULO

Version 5
made with by PrismX
Nebulo Command Line Interface v5
Type 'help' for available commands, 'gui' to switch to graphical mode
user@nebulo:~$
Global Chat
Browser
Games
Blooket Bot
Music
Movies
Roblox Roblox
Clash Royale Clash Royale
Minecraft Minecraft
Global Chat
More Apps
Files
Terminal
Settings
Text Editor
Photos
Help
What's New
App Store
Nebulo

User

Standard User

Browser
Games
Music
Blooket Bot
Movies
Roblox
Roblox
Clash Royale
Clash Royale
Minecraft
Minecraft
Global Chat
Links
Popular Apps
More Apps
Quick Actions
Screenshot
Capture your desktop
Close All
Close all windows
Get OneFile
Download standalone version
Sign Out
Return to login screen
Shut Down
Return to home page

Notifications

No notifications

Application
Application
Status Preinstalled
Type System Application
Location Desktop
`, noPadding: true, width: 900, height: 620, }, cloaking: { title: "Cloaking", icon: "fas fa-mask", content: `
Basic Cloak
Auto-Rotate
Panic Key
Anti-Monitoring
Presets

Basic Tab Cloaking

Disguise your browser tab to look like a different website

Live Preview
favicon Nebulo
This will appear as your browser tab title
Enter a website URL and we'll fetch its favicon automatically

Auto-Rotate Cloaking

Automatically cycle through multiple disguises, keeping your tab constantly changing

Auto-Rotate Status
${cloakingConfig.autoRotate ? "Currently Active" : "Currently Inactive" }
${cloakingConfig.rotateSpeed }s
How often the tab should change disguise

Panic Key

Instantly switch to a safe tab when you need to hide quickly

Panic Key Status
${cloakingConfig.panicKeyEnabled ? "Armed and Ready" : "Disabled" }
${cloakingConfig.panicKey || "Click to set hotkey" }
Press any key combination to set it as your panic hotkey
The website to instantly redirect to when panic key is pressed
This will trigger the panic redirect as a test

Anti-Monitoring

Protect your privacy from admin-installed software

Screen Monitoring Detection
${cloakingConfig.antiScreenMonitoring ? "Enabled - Nebulo will black out when you switch tabs, preventing monitering software from viewing Nebulo" : "Disabled"}
Use Unblack Delay
${cloakingConfig.useAntiMonitorDelay ? "Enabled - Screen will stay black for the configured delay" : "Disabled"}
${cloakingConfig.antiMonitorDelay}ms
How long Nebulo stays blacked out after you switch back to this tab (delays unblacking.) Helpful if you cycle through tabs
Confirm Page Closing
${cloakingConfig.confirmPageClosing ? "Enabled - Alert will show to confirm page close before closing, preventing admin-installed software from remotely closing your tab" : "Disabled"}

Quick Presets

One-click disguises for common websites

Google
google.com
Gmail
mail.google.com
Google Drive
drive.google.com
Classroom
classroom.google.com
Google Docs
docs.google.com
YouTube
youtube.com
Wikipedia
wikipedia.org
GitHub
github.com
`, noPadding: false, width: 900, height: 600, }, settings: { title: "Settings", icon: "fas fa-cog", content: `
General
Appearance
Proxy
System
Account
Advanced

General Settings

Clock & Time
12-Hour Format
Use 12-hour time with AM/PM
Show Seconds
Display seconds in taskbar clock
Desktop
Show Desktop Icons
Display application icons on desktop
What's New
Show on Startup
Open the What's New app automatically after logging in

Appearance

Wallpaper

Set custom images for the desktop and login screen.

Use same for login screen
Mirror the desktop wallpaper on the login page
Taskbar
Full Taskbar
Stretch the taskbar to the full width of the screen (Windows style)
Themes
${installedThemes.length === 0 ? `

No Themes Installed

Visit the App Store to browse and install custom themes for Nebulo.

` : `
${installedThemes .map( (theme) => `
${theme.charAt(0).toUpperCase() + theme.slice(1) } Theme
` ) .join("")}
` }

Proxy Settings

Search Engine

The website all browsers will use to search. The default search engine is Brave.

Brave Search
Brave Search
Duck Duck Go
Google Search
Bing
Startpage
Qwant
Network Configuration
Wisp Server URL

The WebSocket server used for proxying traffic.

System

Boot Options

Reset the boot preference to show the bootloader menu on next reload, allowing you to choose between graphical and command-line modes.

Account

Profile Information
Username
Your account username
${currentUsername}
Account Type
Permission level
${currentUserAccount?.role === "superuser" ? "Super User" : "Standard User"}
Profile Picture

Upload a PNG, JPG, or GIF to personalize your account.

Profile Management

Export your profile to save settings, installed apps, themes, files, and preferences. Import on another device or keep as a backup.

${currentUserAccount?.role === "superuser" ? `
Account Management

As a super user, you can manage all user accounts and their permissions.

` : ''}

Advanced

Experimental Features

Warning: These features are experimental and may not work as expected.

Bypass File Protocol Warnings
Allow experimental browser features on file:// protocol. By enabling this, you agree to report any bugs as GitHub issues. Report issues here
Danger Zone

This will permanently delete all your data including your account, settings, files, themes, and preferences. NOTE: Your achievements and Easter eggs will be preserved! You will be returned to the initial setup screen.

`, noPadding: true, width: 900, height: 600, }, editor: { title: filename || "Text Editor", icon: "fas fa-edit", content: `
`, noPadding: true, width: 900, height: 600, }, music: { title: "Music", icon: "fas fa-music", content: (() => { const targetUrl = "https://vapor.onl/page/music/index.html"; const scramjetUrl = encodeScramjetUrl(targetUrl); const fallbackProxy = `${new URL(".", window.location.href).pathname}uv/nebulo.html?mode=music&url=${encodeURIComponent(targetUrl)}`; return `
`; })(), noPadding: true, width: 900, height: 600, }, movies: { title: "Movies", icon: "fas fa-film", content: (() => { const targetUrl = "https://www.cineby.gd/"; const scramjetUrl = encodeScramjetUrl(targetUrl); const fallbackProxy = `${new URL(".", window.location.href).pathname}uv/nebulo.html?mode=games&url=${encodeURIComponent(targetUrl)}`; return `
Cineby runs through the Scramjet/Sail proxy and keeps attempted redirects trapped inside the sandboxed iframe.
`; })(), noPadding: true, width: 1000, height: 650, }, roblox: { title: "Roblox", icon: "fas fa-gamepad", content: (() => { const targetUrl = "https://astra.pxi-fusion.com/embed/roblox"; return `
Roblox may refuse to load because no UV proxy is running on this host.
`; })(), noPadding: true, width: 1200, height: 700, }, "clash-royale": { title: "Clash Royale", icon: "fas fa-shield-alt", content: (() => { const targetUrl = "https://astra.pxi-fusion.com/embed/clash-royale"; return `
Clash Royale may refuse to load because no UV proxy is running on this host.
`; })(), noPadding: true, width: 1200, height: 700, }, movies: { title: "Movies", icon: "fas fa-film", content: (() => { const targetUrl = "https://www.cineby.gd/"; const encoded = encodeScramjetUrl(targetUrl); return `
Cineby ships inside a sandbox; redirects are blocked and the stream stays inside the proxy.
`; })(), noPadding: true, width: 1200, height: 750, }, photos: { title: "Photos", icon: "fas fa-images", content: (() => { const photos = fileSystem["Photos"] || {}; const photoList = Object.keys(photos); if (photoList.length === 0) { return `

No Photos Yet

Take a screenshot to get started!

`; } return `
${photoList .map( (name) => `
${name}
${name}
` ) .join("")}
`; })(), noPadding: true, width: 900, height: 600, }, help: { title: "Help", icon: "fas fa-question-circle", content: (() => { // Base (static) help topics we always want present const baseTopics = [ { id: 'welcome', icon: 'fa-info-circle', title: 'Welcome', preview: 'Introduction to Nebulo' }, { id: 'cloaking', icon: 'fa-mask', title: 'Cloaking', preview: 'Tab disguise features' }, { id: 'boot', icon: 'fa-power-off', title: 'Boot Options', preview: 'Graphical vs command-line' }, { id: 'apps', icon: 'fa-th', title: 'Applications', preview: 'Built-in apps guide' }, { id: 'desktop', icon: 'fa-desktop', title: 'Desktop', preview: 'Icons, taskbar & windows' }, { id: 'notifications', icon: 'fa-bell', title: 'Notifications', preview: 'Quick actions & alerts' }, { id: 'screenshots', icon: 'fa-camera', title: 'Screenshots', preview: 'Capture your desktop' }, { id: 'settings', icon: 'fa-cog', title: 'Settings', preview: 'Customize your experience' }, { id: 'tips', icon: 'fa-lightbulb', title: 'Tips & Tricks', preview: 'Pro user shortcuts' } ]; // Build a set of ids/titles already present so we can add missing apps const existingIds = new Set(baseTopics.map(t => t.id)); // Generate app topics for any app in appMetadata that isn't already covered const appTopics = []; try { for (const key in appMetadata) { if (!Object.prototype.hasOwnProperty.call(appMetadata, key)) continue; const meta = appMetadata[key]; // normalize id/title keys we already used const normalized = key.replace(/[^a-z0-9\-]/gi, '').toLowerCase(); if (existingIds.has(normalized) || existingIds.has(meta.name.toLowerCase())) continue; // skip the Help topic itself if (key === 'help') continue; appTopics.push({ id: `app-${normalized}`, icon: (meta.icon ? meta.icon : 'fa-question-circle'), title: meta.name, preview: meta.preinstalled ? 'Preinstalled app' : 'Optional app' }); } } catch (e) { console.error('Error building dynamic help topics', e); } const topics = baseTopics.concat(appTopics); const topicCards = topics.map(t => `
${t.title}
${t.preview || ''}
`).join(''); return `
${topicCards}
`; })(), noPadding: true, width: 900, height: 600, }, whatsnew: { title: "What's New in Nebulo", icon: "fas fa-star", content: `

Welcome to Nebulo
v5! What's new?
Movies • Roblox • UI polish

AI Assistant, Browser Updates, More Themes, VS Code, Window Snapping, Games, and more—now with Cineby streaming and clearer proxy choices.

`, noPadding: true, width: 900, height: 600, }, calculator: { title: "About Nebulo", icon: "fas fa-info-circle", content: `
Nebulo

Nebulo

Version 1.5

A beautiful, fully-featured web-based operating system experience. Built with vanilla HTML, CSS, and JavaScript — no frameworks needed! Nebulo brings you a complete desktop environment right in your browser, with file management, multiple apps, themes, and more.

Developers

Click on a developer's card to learn more about them and support their work.

PrismX
Lead Developer & Creator

i make proxies, tools, and most importantly, silly websites :D

X8r
Developer

A front end developer who really likes working on projects involving proxying, unblocking, games, and more.

lanefiedler731-gif
Contributor & Developer

A talented contributor who has helped expand Nebulo with new features, improvements, and bug fixes. Passionate about creating great user experiences and pushing the boundaries of web-based applications.

Support with Monero (XMR)

XMR QR Code

Monero Address:

47T7eTcAXKGRqVRXAbxqsah8JSTYVUsUQgoe1bD1S4rwigc8EEaTkniBUPkcGwzjQZWNcd6AucPxSh4rceUAjG1o1uQiexu
Open in Wallet

Contributors

Special thanks to our contributors who help improve Nebulo!

RHW
Contributor
derpman1483
Contributor
`, width: 920, height: 600, }, credits: { title: "Credits", icon: "fas fa-heart", content: `

Credits

Special thanks to the amazing people who made this possible.

Original Creator

PrismX
The original creator of Nebulo
`, noPadding: true, width: 700, height: 600, }, calculator: { title: "Calculator", icon: "fas fa-calculator", content: `
0
`, noPadding: true, width: 400, height: 600, }, python: { title: "Python Interpreter", icon: "fas fa-code", content: `
Python Code Editor
Output Console
`, noPadding: true, width: 900, height: 600, }, "snap-manager": { title: "Snap Manager", icon: "fas fa-border-all", content: renderSnapManager(), noPadding: true, width: 820, height: 640, }, vsc: { title: "Visual Studio Code", icon: "fas fa-code", content: (() => { if (!checkFileProtocol("Visual Studio Code")) { return `

VS Code Unavailable

Visual Studio Code doesn't work on file:// protocol. Please run Nebulo from a web server to use this feature.

`; } const vscUrl = encodeScramjetUrl("https://vscode.dev/"); return `

Launching VS Code with Sail...

Load times may vary

`; })(), noPadding: true, width: 900, height: 600, }, helios: { title: "Helios Browser", icon: "fas fa-globe", content: (() => { if (!checkFileProtocol("Helios Browser")) { return `

Helios Unavailable

Helios Browser doesn't work on file:// protocol. Please run Nebulo from a web server to use this feature.

`; } const assetBasePath = new URL(".", window.location.href).pathname; return `
`; })(), noPadding: true, width: 900, height: 600, }, appstore: { title: "App Store", icon: "fas fa-store", content: (() => { const lightThemeInstalled = installedThemes.includes("light"); return `
Themes
Apps
Games
Community

Themes

Customize your Nebulo experience

Dark Theme by PrismX
The default Nebulo theme. Sleek dark interface with teal accents, perfect for extended use and reducing eye strain.
Light Theme by PrismX
A bright and clean theme perfect for daytime use. Easy on the eyes with light backgrounds and dark text.
Golden Theme by lanefiedler-731
Elegant golden accents with warm, luxurious dark backgrounds. Perfect for a premium look.
Red Theme by lanefiedler-731
Bold and vibrant red accents for those who want to stand out. Energy meets elegance.
Blue Theme by lanefiedler-731
Cool and calming blue tones. Professional and soothing for extended use.
Purple Theme by lanefiedler-731
Deep shades combined with royal hues, crafted together for the perfect purple theme.
Green Theme by lanefiedler-731
Rich shades of green with splashes of lime and seaweed, this is quite the exquisite theme.
Liquid Glass by $xor
An Apple insipired theme with a beautiful translucent look.
`; })(), noPadding: true, width: 900, height: 600, }, snake: { title: "Snake", icon: "fas fa-gamepad", content: `

Snake

Eat food and grow!
Score
0
High Score
0
Controls:
Arrow Keys or WASD
SPACE: Pause/Resume
R: Restart
`, noPadding: true, width: 700, height: 600, }, tictactoe: { title: "Tic-Tac-Toe", icon: "fas fa-circle", content: `

Tic-Tac-Toe

Your turn (X)
Wins
0
Losses
0
Draws
0
`, noPadding: true, width: 650, height: 550, }, "v86-emulator": { title: "V86 Emulator", icon: "fas fa-microchip", content: (() => { return generateV86Interface(); })(), noPadding: true, width: 1000, height: 700, }, "ai-snake": { title: "AI Snake Learning", icon: "fas fa-brain", content: `

AI Snake Learning

by lanefiedler-731
Deep Q-Learning Neural Network
Generation
0
Best Score
0
Current Score
0
Games Played
0
Average Score
0
Epsilon (Exploration)
1.00
Training Settings
10 ms
10 iterations
5 games
GPU Ready
Training Status
Ready to start training...
Watch the AI learn to play Snake!
`, noPadding: true, width: 850, height: 600, }, "nebulo-ai": { title: "Nebulo AI Assistant", icon: "fas fa-robot", content: `

Nebulo AI Assistant

by lanefiedler-731 | Powered by WebLLM
Initializing AI model...
Model:
Nebulo AI
Hello! I'm your Nebulo AI Assistant. I can help you understand and navigate Nebulo.

Model: Smarty (Qwen3-0.6B)
Advanced reasoning with thinking mode enabled. Switch models in the dropdown if needed.

OS Automation Enabled!
I can control Nebulo for you! I can open apps, run terminal commands, manage files, change settings, and more. Just ask me to do something and I'll request your approval before taking action.

Thinking Mode Active!
For complex tasks, I'll show my reasoning process in a collapsible "Thinking" section.

Try: "Open calculator" or "Change theme to blue"
`, noPadding: true, width: 700, height: 600, }, "web-app-creator": { title: "Web App Creator", icon: "fas fa-puzzle-piece", content: `

Web App Creator

Create custom web apps that run like native apps

Examples: fa-music, fa-gamepad, fa-video, fa-shopping-cart

Your Web Apps

Loading...

`, noPadding: true, width: 550, height: 650, onOpen: function () { setTimeout(refreshCustomWebAppsList, 100); } }, chat: { title: "Global Chat", icon: "fas fa-comments", content: `
`, noPadding: true, width: 700, height: 600, }, }; apps["popular-apps"] = { title: "Popular Apps", icon: "fas fa-th-large", content: ` `, noPadding: true, width: 980, height: 660, }; delete apps["suggestion-board"]; if (appName === "achievements") { openAchievements(); return; } if (apps[appName]) { const app = apps[appName]; trackAppOpened(appName); if (appName === "clash-royale") { await showModal({ type: "warning", icon: "fa-exclamation-triangle", title: "Proxy Unavailable", message: "Proxy access for Clash Royale is not available here. The game may be blocked on your network. Click OK to continue.", confirm: true, confirmText: "OK", }); } const windowEl = createWindow( app.title, app.icon, app.content, app.width || 800, app.height || 600, appName, app.noPadding || false ); if (appName === "minecraft") { const iframe = windowEl.querySelector("iframe"); if (iframe) { iframe.setAttribute("tabindex", "0"); const focusMinecraftFrame = () => { if (focusedWindow !== "minecraft") { focusedWindow = "minecraft"; focusWindow(windowEl); updateTaskbarIndicators(); } try { if (iframe.contentWindow) { iframe.contentWindow.focus(); } } catch (err) { // Ignore cross-origin focus errors. } iframe.focus(); }; iframe.addEventListener( "load", () => { focusMinecraftFrame(); try { const frameDoc = iframe.contentDocument; if (frameDoc) { frameDoc.addEventListener("mousedown", focusMinecraftFrame); frameDoc.addEventListener("keydown", focusMinecraftFrame); frameDoc.addEventListener("touchstart", focusMinecraftFrame, { passive: true }); } } catch (err) { // Ignore cross-origin access errors. } }, { once: true } ); windowEl.addEventListener("mousedown", focusMinecraftFrame); windowEl.addEventListener("touchstart", focusMinecraftFrame, { passive: true }); } } if (appName === "settings") { setTimeout(() => { initializeAppearanceSettings(); }, 50); } if (appName === "terminal") { setTimeout(() => { const input = document.getElementById("terminalInput"); if (input) input.focus(); }, 100); } if (appName === "browser") { initializeBrowserWispControls(windowEl); setTimeout(() => { showToast( "Want a different experience? Try Helios Browser in the App Store!", "fa-info-circle" ); }, 500); } if (appName === "whatsnew") { currentSlide = 0; } if (appName === "snake") { setTimeout(() => { snakeGame.canvas = document.getElementById('snakeCanvas'); if (snakeGame.canvas) { snakeGame.ctx = snakeGame.canvas.getContext('2d'); snakeGame.highScore = localStorage.getItem('snakeHighScore') ? parseInt(localStorage.getItem('snakeHighScore')) : 0; document.getElementById('snakeHighScore').textContent = snakeGame.highScore; drawSnakeGame(); } }, 50); } if (appName === "2048") { setTimeout(() => { start2048Game(); }, 50); } if (appName === "tictactoe") { setTimeout(() => { startTicTacToe(); }, 50); } if (appName === "v86-emulator") { setTimeout(() => { // Load V86 resources on-demand with progress bar loadV86ResourcesOnDemand(windowEl); }, 50); } if (appName === "ai-snake") { setTimeout(() => { initializeAISnakeApp(); }, 50); } if (appName === "nebulo-ai") { setTimeout(() => { initializeNebuloAI(); }, 50); } if (appName === "web-app-creator") { setTimeout(() => { refreshCustomWebAppsList(); }, 50); } } } function handleTerminalInput(e) { if (e.key === "Enter") { const input = e.target; const command = input.value.trim(); const terminal = document.getElementById("terminalContent"); const cmdLine = document.createElement("div"); cmdLine.className = "terminal-line"; cmdLine.innerHTML = `user@nebulo:~$ ${command}`; terminal.insertBefore(cmdLine, terminal.lastElementChild); const output = document.createElement("div"); output.className = "terminal-line"; if (command === "help") { output.innerHTML = "Available commands:
" + "help - Show this list
" + "ls - List files in file system
" + "apps - List installed applications
" + "themes - List installed themes
" + "clear - Clear terminal
" + "date - Show current date and time
" + "whoami - Display current username
" + "reset-boot - Reset bootloader preferences
" + "refresh-cache - Purge jsDelivr cache for community apps
" + "echo [text] - Display text"; } else if (command === "ls") { const tree = ".\n" + generateFileTree(fileSystem); output.innerHTML = '
' + tree + "
"; } else if (command === "apps") { const appList = [ "Files - File manager and explorer", "Terminal - Command line interface", "Browser - Web browser", "Settings - System settings", "Text Editor - Edit text files", "Music - Music player", "Photos - Photo viewer", "Help - System help and documentation", "What's New - View latest features", "App Store - Browse and install apps/themes", ]; output.innerHTML = 'Installed Applications:
' + appList.map((app) => ` • ${app}`).join("
"); } else if (command === "themes") { const themeList = ["Dark Theme (Default)"]; if (installedThemes.length > 0) { installedThemes.forEach((theme) => { themeList.push( `${theme.charAt(0).toUpperCase() + theme.slice(1)} Theme` ); }); } output.innerHTML = 'Installed Themes:
' + themeList.map((theme) => ` • ${theme}`).join("
"); } else if (command === "whoami") { output.textContent = currentUsername; } else if (command === "reset-boot") { localStorage.removeItem("nebulo_bootChoice"); output.innerHTML = '✓ Bootloader preferences reset successfully
' + "The bootloader menu will appear on next page reload."; } else if (command === "refresh-cache") { output.innerHTML = "Submitting purge request to jsDelivr...
"; // Asynchronously purge key files fetch('https://purge.jsdelivr.net/gh/nautilus-os/community@main/files/info.json').catch(e => { }); fetch('https://purge.jsdelivr.net/gh/nautilus-os/community@main/apps').catch(e => { }); // We can't actually verify purge easily from client without CORS issues sometimes? // But the request fires. output.innerHTML += '✓ Purge request sent.
' + "Please wait a few moments and try reloading the store."; } else if (command === "clear") { terminal.innerHTML = `
Nebulo Terminal v1.5
Type 'help' for available commands
`; } else if (command === "date") { output.textContent = new Date().toString(); } else if (command.startsWith("echo ")) { output.textContent = command.substring(5); } else if (command) { output.innerHTML = `Command not found: ${command}
Type 'help' for available commands.`; } if (command !== "clear" && command) { terminal.insertBefore(output, terminal.lastElementChild); } const newInputLine = document.createElement("div"); newInputLine.className = "terminal-line"; newInputLine.innerHTML = 'user@nebulo:~$ '; terminal.removeChild(terminal.lastElementChild); terminal.appendChild(newInputLine); const newInput = document.getElementById("terminalInput"); if (newInput) newInput.focus(); terminal.scrollTop = terminal.scrollHeight; } } function toggleSetting(setting) { if (setting === "showWhatsNew") { const currentValue = localStorage.getItem("nebulo_showWhatsNew"); const newValue = currentValue === "false" ? "true" : "false"; localStorage.setItem("nebulo_showWhatsNew", newValue); const toggles = document.querySelectorAll(".toggle-switch"); toggles.forEach((toggle) => { const parent = toggle.parentElement; if (parent.textContent.includes("Show on Startup")) { if (newValue === "true") { toggle.classList.add("active"); } else { toggle.classList.remove("active"); } } }); return; } settings[setting] = !settings[setting]; saveSettingsToLocalStorage(); trackSettingChanged(setting); if (setting === "showDesktopIcons") { const icons = document.getElementById("desktopIcons"); if (settings.showDesktopIcons) { icons.classList.remove("hidden"); } else { icons.classList.add("hidden"); } } const toggles = document.querySelectorAll(".toggle-switch"); toggles.forEach((toggle) => { const parent = toggle.parentElement; if (parent.textContent.includes("12-Hour") && setting === "use12Hour") { toggle.classList.toggle("active"); } else if ( parent.textContent.includes("Show Seconds") && setting === "showSeconds" ) { toggle.classList.toggle("active"); } else if ( parent.textContent.includes("Show Desktop Icons") && setting === "showDesktopIcons" ) { toggle.classList.toggle("active"); } else if ( parent.textContent.includes("Bypass File Protocol Warnings") && setting === "bypassFileProtocolWarnings" ) { toggle.classList.toggle("active"); } }); } document.addEventListener("DOMContentLoaded", () => { const passwordInput = document.getElementById("password"); if (passwordInput) { passwordInput.addEventListener("keypress", (e) => { if (e.key === "Enter") login(); }); } }); document.addEventListener("click", (e) => { const startMenu = document.getElementById("startMenu"); if ( startMenu && !startMenu.contains(e.target) && !e.target.closest(".taskbar-icon") ) { startMenu.classList.remove("active"); } }); function readImageFile(file, onLoad) { const allowedTypes = ["image/png", "image/jpeg", "image/gif"]; if (!file) return false; if (!allowedTypes.includes(file.type)) { showToast("Please choose a PNG, JPG, or GIF image.", "fa-exclamation-circle"); setImageError("Only PNG, JPG, or GIF files are supported."); return false; } const maxBytes = 3 * 1024 * 1024; if (file.size > maxBytes) { showToast("Image must be smaller than 3MB.", "fa-exclamation-circle"); setImageError("Image must be smaller than 3MB."); return false; } const reader = new FileReader(); reader.onload = () => { setImageError(""); onLoad(reader.result); }; reader.readAsDataURL(file); return true; } function setImageError(message) { const el = document.getElementById("imageErrorMessage"); if (!el) return; if (message) { el.textContent = message; el.style.display = "block"; } else { el.textContent = ""; el.style.display = "none"; } } function applyUserBackgrounds() { const desktop = document.getElementById("desktop"); const login = document.getElementById("login"); const wallpaperLayer = document.querySelector(".wallpaper"); const wallpaperData = localStorage.getItem("nebulo_wallpaper"); const loginBackgroundData = localStorage.getItem("nebulo_loginBackground"); const sameSetting = localStorage.getItem("nebulo_useSameBackground"); const useSame = sameSetting === null || sameSetting === "true"; const loginData = useSame ? wallpaperData : loginBackgroundData; if (desktop) { if (wallpaperData) { desktop.style.background = `center / cover no-repeat url(${wallpaperData})`; } else { desktop.style.background = "linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%)"; } } if (wallpaperLayer) { wallpaperLayer.style.display = wallpaperData ? "none" : ""; } if (login) { if (loginData) { login.style.background = `center / cover no-repeat url(${loginData})`; } else { login.style.background = "linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%)"; } } } function initializeAppearanceSettings() { const sameSetting = localStorage.getItem("nebulo_useSameBackground"); const useSame = sameSetting === null || sameSetting === "true"; const toggle = document.getElementById("loginWallpaperToggle"); const controls = document.getElementById("loginWallpaperControls"); const desktopButton = document.getElementById("desktopWallpaperButton"); const loginButton = document.getElementById("loginWallpaperButton"); const profileButton = document.getElementById("profilePictureButton"); const hasWallpaper = !!localStorage.getItem("nebulo_wallpaper"); const hasLoginWallpaper = !!localStorage.getItem("nebulo_loginBackground"); const hasProfile = !!localStorage.getItem("nebulo_profilePicture"); if (toggle) { if (useSame) { toggle.classList.add("active"); } else { toggle.classList.remove("active"); } } if (controls) { controls.style.display = useSame ? "none" : ""; } if (desktopButton) { desktopButton.innerHTML = ` ${hasWallpaper ? "Change Desktop Wallpaper" : "Set Desktop Wallpaper" }`; } if (loginButton) { loginButton.innerHTML = ` ${hasLoginWallpaper ? "Change Login Wallpaper" : "Set Login Wallpaper" }`; } if (profileButton) { profileButton.innerHTML = ` ${hasProfile ? "Change Profile Picture" : "Set Profile Picture" }`; } } function handleWallpaperUpload(event) { const file = event.target.files[0]; if (!readImageFile(file, (data) => { localStorage.setItem("nebulo_wallpaper", data); applyUserBackgrounds(); initializeAppearanceSettings(); showToast("Desktop wallpaper updated!", "fa-check-circle"); })) { return; } event.target.value = ""; } function clearWallpaper() { if (!localStorage.getItem("nebulo_wallpaper")) { showToast("No desktop wallpaper is set.", "fa-info-circle"); return; } localStorage.removeItem("nebulo_wallpaper"); applyUserBackgrounds(); initializeAppearanceSettings(); const input = document.getElementById("wallpaperInput"); if (input) input.value = ""; showToast("Desktop wallpaper reset.", "fa-check-circle"); } function toggleLoginWallpaperLink(element) { const sameSetting = localStorage.getItem("nebulo_useSameBackground"); const useSame = sameSetting === null || sameSetting === "true"; const newValue = !useSame; localStorage.setItem("nebulo_useSameBackground", newValue ? "true" : "false"); if (element) { if (newValue) { element.classList.add("active"); } else { element.classList.remove("active"); } } const controls = document.getElementById("loginWallpaperControls"); if (controls) { controls.style.display = newValue ? "none" : ""; } applyUserBackgrounds(); initializeAppearanceSettings(); } function handleLoginBackgroundUpload(event) { const file = event.target.files[0]; if (!readImageFile(file, (data) => { localStorage.setItem("nebulo_loginBackground", data); applyUserBackgrounds(); initializeAppearanceSettings(); showToast("Login wallpaper updated!", "fa-check-circle"); })) { return; } event.target.value = ""; } function clearLoginWallpaper() { if (!localStorage.getItem("nebulo_loginBackground")) { showToast("No login wallpaper is set.", "fa-info-circle"); return; } localStorage.removeItem("nebulo_loginBackground"); applyUserBackgrounds(); initializeAppearanceSettings(); const input = document.getElementById("loginWallpaperInput"); if (input) input.value = ""; showToast("Login wallpaper reset.", "fa-check-circle"); } function handleProfilePictureUpload(event) { const file = event.target.files[0]; if (!readImageFile(file, (data) => { localStorage.setItem("nebulo_profilePicture", data); applyProfilePicture(); initializeAppearanceSettings(); showToast("Profile picture updated!", "fa-check-circle"); })) { return; } event.target.value = ""; } function clearProfilePicture() { if (!localStorage.getItem("nebulo_profilePicture")) { showToast("No profile picture is set.", "fa-info-circle"); return; } localStorage.removeItem("nebulo_profilePicture"); applyProfilePicture(); initializeAppearanceSettings(); const input = document.getElementById("profilePictureInput"); if (input) input.value = ""; showToast("Profile picture reset.", "fa-check-circle"); } function applyProfilePicture() { const data = localStorage.getItem("nebulo_profilePicture"); const avatars = document.querySelectorAll(".login-avatar, .start-avatar"); avatars.forEach((avatar) => { if (data) { avatar.style.background = `center / cover no-repeat url(${data})`; avatar.classList.add("has-image"); } else { avatar.style.background = ""; avatar.classList.remove("has-image"); } }); } function updateLoginGreeting() { const now = new Date(); const hour = now.getHours(); const greetingEl = document.getElementById("loginGreeting"); let greeting = "Welcome Back"; const username = localStorage.getItem("nebulo_username") || "User"; if (hour >= 5 && hour < 12) { greeting = `Good Morning, ${username}`; } else if (hour >= 12 && hour < 17) { greeting = `Good Afternoon, ${username}`; } else if (hour >= 17 && hour < 22) { greeting = `Good Evening, ${username}`; } else { greeting = `Good Night, ${username}`; } if (greetingEl) { greetingEl.textContent = greeting; } } function getFileSystemAtPath(path) { let current = fileSystem; for (let segment of path) { if (current[segment] && typeof current[segment] === "object") { current = current[segment]; } else { return null; } } return current; } function navigateToPath(path) { currentPath = [...path]; if (windows["files"]) { updateFileExplorer(); } } function updateFileExplorer() { if (!windows["files"]) return; let current = getFileSystemAtPath(currentPath); if (!current) { current = fileSystem; currentPath = []; } setTimeout(() => { expandTreeToPath(currentPath); }, 50); const fileExplorer = windows["files"].querySelector(".file-explorer"); if (!fileExplorer) return; fileExplorer.innerHTML = `
File System
${renderFileTree()}
${renderBreadcrumb()}
${Object.keys(current) .sort() .map((file) => { const isFolder = typeof current[file] === "object"; const icon = isFolder ? "fa-folder" : "fa-file-alt"; const escapedFile = file.replace(/'/g, "\\'"); return `
${file}
`; }) .join("")}
`; } function renderFileTree(fs = fileSystem, parentPath = [], level = 0) { let html = ""; Object.keys(fs) .sort() .forEach((name) => { const item = fs[name]; const isFolder = typeof item === "object"; const currentItemPath = [...parentPath, name]; const pathString = currentItemPath.join("/"); const isActive = JSON.stringify(currentItemPath) === JSON.stringify(currentPath); if (isFolder) { html += `
${name}
${renderFileTree(item, currentItemPath, level + 1)}
`; } else { html += `
${name}
`; } }); return html; } function toggleTreeFolder(element, pathString, event) { event.stopPropagation(); const children = document.querySelector( `.file-tree-children[data-path="${pathString}"]` ); if (children) { const isExpanded = children.classList.contains("expanded"); if (isExpanded) { children.classList.remove("expanded"); element.classList.remove("expanded"); } else { children.classList.add("expanded"); element.classList.add("expanded"); } } } function navigateToFolderFromTree(pathString) { const path = pathString ? pathString.split("/") : []; navigateToPath(path); } function openFileFromTree(pathString) { const path = pathString.split("/"); const filename = path[path.length - 1]; const parentPath = path.slice(0, -1); let current = fileSystem; for (let segment of parentPath) { current = current[segment]; } const content = current[filename]; if (typeof content === "string") { openApp("editor", content, filename); } } function renderBreadcrumb() { if (currentPath.length === 0) { return ' Home'; } let html = ' Home'; currentPath.forEach((segment, index) => { const path = currentPath.slice(0, index + 1); html += ' / '; html += `${segment}`; }); return html; } const fileChecksum1 = "bWFkZSB3aXRoIDxpIGNsYXNzPSJmYS1zb2xpZCBmYS1oZWFydCI+PC9pPiBieSBQcmlzbVg="; const fileChecksum2 = "YnkgUHJpc21Y"; function moveFileToFolder() { const bottomTextElement = document.getElementById("bottom-text"); const bylineElement = document.querySelector(".wallpaper-byline"); let bottomContent = ""; let bylineContent = ""; if (bottomTextElement) { bottomContent = bottomTextElement.innerHTML.trim(); } if (bylineElement) { bylineContent = bylineElement.textContent.trim(); } const expectedBottom = atob(fileChecksum1); const expectedByline = atob(fileChecksum2); const bottomValid = bottomContent === expectedBottom; const bylineValid = bylineContent === expectedByline; if (!bottomValid || !bylineValid) { window.openApp = window.closeWindow = window.minimizeWindow = window.showToast = window.handleTaskbarClick = function () { }; const elements = document.querySelectorAll("div, body, html, :root"); elements.forEach((el) => { el.style.transition = "none"; el.style.animation = "none"; el.style.backgroundColor = "var(--pure-black)"; el.style.color = "var(--pure-black)"; el.style.borderColor = "var(--pure-black)"; el.style.boxShadow = "none"; el.style.backgroundImage = "none"; }); document.body.innerHTML = '
'; return false; } return true; } moveFileToFolder(); function goUpDirectory() { if (currentPath.length > 0) { currentPath.pop(); if (windows["files"]) { updateFileExplorer(); } } } async function createNewFolder() { const folderName = await prompt("Enter folder name:"); if (!folderName) return; let current = getFileSystemAtPath(currentPath); if (current && !current[folderName]) { current[folderName] = {}; showToast("Folder created: " + folderName, "fa-folder-plus"); if (windows["files"]) { updateFileExplorer(); } } else { showToast( "Folder already exists or invalid location", "fa-exclamation-circle" ); } } function showContextMenu(x, y, items) { const menu = document.getElementById("contextMenu"); menu.innerHTML = items .map((item) => { if (item.divider) { return '
'; } const disabledClass = item.disabled ? "disabled" : ""; return `
${item.label}
`; }) .join(""); menu.style.left = x + "px"; menu.style.top = y + "px"; menu.classList.add("active"); setTimeout(() => { const rect = menu.getBoundingClientRect(); if (rect.right > window.innerWidth) { menu.style.left = x - rect.width + "px"; } if (rect.bottom > window.innerHeight) { menu.style.top = y - rect.height + "px"; } }, 0); } function hideContextMenu() { const menu = document.getElementById("contextMenu"); menu.classList.remove("active"); } function refreshDesktop() { hideContextMenu(); const openWindows = Object.keys(windows); if (openWindows.length === 0) { showToast("Desktop refreshed", "fa-sync"); return; } const appsToReopen = [...openWindows]; appsToReopen.forEach((appName) => { const windowEl = windows[appName]; if (windowEl) { windowEl.remove(); } }); windows = {}; focusedWindow = null; updateTaskbarIndicators(); showToast("Refreshing all applications...", "fa-sync"); setTimeout(() => { appsToReopen.forEach((appName, index) => { setTimeout(() => { openApp(appName); }, index * 100); }); showToast( `Refreshed ${appsToReopen.length} application(s)`, "fa-check-circle" ); }, 500); } function openNewTextFile() { hideContextMenu(); openApp("editor", "", ""); } function openNewFolder() { hideContextMenu(); createNewFolder(); } function initContextMenu() { document.addEventListener("contextmenu", (e) => { if (e.target.closest(".desktop-icon")) { e.preventDefault(); const icon = e.target.closest(".desktop-icon"); const appName = icon.getAttribute("data-app"); showContextMenu(e.clientX, e.clientY, [ { icon: "fa-folder-open", label: "Open", action: `hideContextMenu(); openApp('${appName}')`, }, { divider: true, }, { icon: "fa-info-circle", label: "Properties", action: `hideContextMenu(); showProperties('${appName}', ${e.clientX}, ${e.clientY})`, }, ]); return; } if (e.target.closest(".window")) { e.preventDefault(); const windowEl = e.target.closest(".window"); const appName = windowEl.dataset.appName || ""; const isMaximized = windowEl.dataset.maximized === "true"; showContextMenu(e.clientX, e.clientY, [ { icon: "fa-window-minimize", label: "Minimize", action: `hideContextMenu(); setTimeout(() => minimizeWindowByAppName('${appName}'), 50)`, }, { icon: "fa-window-maximize", label: isMaximized ? "Restore" : "Maximize", action: `hideContextMenu(); setTimeout(() => maximizeWindowByAppName('${appName}'), 50)`, }, { divider: true, }, { icon: "fa-times", label: "Close Window", action: `hideContextMenu(); setTimeout(() => closeWindowByAppName('${appName}'), 50)`, }, { divider: true, }, { icon: "fa-question-circle", label: "Help", action: `hideContextMenu(); openApp('help')`, }, ]); return; } if (e.target.closest("#desktop")) { e.preventDefault(); showContextMenu(e.clientX, e.clientY, [ { icon: "fa-sync", label: "Refresh", action: "hideContextMenu(); refreshDesktop()", }, { divider: true, }, { icon: "fa-file", label: "New Text File", action: "hideContextMenu(); openNewTextFile()", }, { icon: "fa-folder-plus", label: "New Folder", action: "hideContextMenu(); openNewFolder()", }, { divider: true, }, { icon: "fa-question-circle", label: "Help", action: `hideContextMenu(); openApp('help')`, }, ]); return; } }); document.addEventListener("click", (e) => { if (!e.target.closest(".context-menu")) { hideContextMenu(); } }); } let currentSlide = 0; function changeSlide(direction) { const slides = document.querySelectorAll(".carousel-slide"); const dots = document.querySelectorAll(".carousel-dot"); if (!slides.length) return; slides[currentSlide].classList.remove("active"); dots[currentSlide].classList.remove("active"); currentSlide += direction; if (currentSlide >= slides.length) currentSlide = 0; if (currentSlide < 0) currentSlide = slides.length - 1; slides[currentSlide].classList.add("active"); dots[currentSlide].classList.add("active"); } function goToSlide(index) { const slides = document.querySelectorAll(".carousel-slide"); const dots = document.querySelectorAll(".carousel-dot"); if (!slides.length) return; slides[currentSlide].classList.remove("active"); dots[currentSlide].classList.remove("active"); currentSlide = index; slides[currentSlide].classList.add("active"); dots[currentSlide].classList.add("active"); } function toggleDevInfo(devId) { // Find the current about app container const currentDevHeader = document.querySelector(`[onclick="toggleDevInfo('${devId}')"]`); if (!currentDevHeader) return; const currentContainer = currentDevHeader.closest('.about-app-container'); if (!currentContainer) return; // Collapse all other dev-info panels in this container first so only one is open at a time const allInfo = currentContainer.querySelectorAll('.dev-info'); allInfo.forEach(el => { if (el && el.id !== 'devinfo-' + devId) { el.style.maxHeight = '0px'; const otherChevron = currentContainer.querySelector('#' + el.id.replace('devinfo-', 'chevron-')); if (otherChevron) otherChevron.style.transform = 'rotate(0deg)'; } }); const infoEl = currentContainer.querySelector('#devinfo-' + devId); const chevronEl = currentContainer.querySelector('#chevron-' + devId); if (!infoEl) return; const isExpanded = infoEl.style.maxHeight && infoEl.style.maxHeight !== '0px'; if (isExpanded) { infoEl.style.maxHeight = '0px'; if (chevronEl) chevronEl.style.transform = 'rotate(0deg)'; } else { // compute natural height then set; allow inner overflow if content is long infoEl.style.maxHeight = infoEl.scrollHeight + 'px'; infoEl.style.overflow = 'hidden'; if (chevronEl) chevronEl.style.transform = 'rotate(180deg)'; } } function initScrollIndicator() { const indicator = document.getElementById("scrollIndicator"); if (!indicator) return; setTimeout(() => { if (document.documentElement.scrollHeight > window.innerHeight) { indicator.classList.add("visible"); } }, 1000); window.addEventListener("scroll", () => { const scrollTop = window.pageYOffset || document.documentElement.scrollTop; const scrollHeight = document.documentElement.scrollHeight; const clientHeight = document.documentElement.clientHeight; if (scrollTop > 100) { indicator.classList.remove("visible"); } else if (scrollHeight > clientHeight) { indicator.classList.add("visible"); } }); } let selectedFileItem = null; let draggedFileName = null; function selectFileItem(event, element, filename) { if (event.detail === 2) return; event.stopPropagation(); if (selectedFileItem && selectedFileItem !== element) { selectedFileItem.classList.remove("selected"); } if (element.classList.contains("selected")) { element.classList.remove("selected"); selectedFileItem = null; } else { element.classList.add("selected"); selectedFileItem = element; } } function handleFileDragStart(event, fileName) { draggedFileName = fileName; event.dataTransfer.setData("text/plain", fileName); } function handleFileDragOver(event, isFolder) { if (!isFolder || !draggedFileName) return; event.preventDefault(); event.dataTransfer.dropEffect = "move"; document .querySelectorAll(".file-item.drag-over") .forEach((el) => el.classList.remove("drag-over")); event.currentTarget.classList.add("drag-over"); } function handleFileDragLeave(event) { if (!event.currentTarget.contains(event.relatedTarget)) { event.currentTarget.classList.remove("drag-over"); } } function handleFileDrop(event, targetFolder) { event.preventDefault(); document .querySelectorAll(".file-item.drag-over") .forEach((el) => el.classList.remove("drag-over")); if (!draggedFileName || draggedFileName === targetFolder) return; if (!draggedFileName || draggedFileName === targetFolder) return; let current = getFileSystemAtPath(currentPath); if ( !current || !current[targetFolder] || typeof current[targetFolder] !== "object" ) return; const item = current[draggedFileName]; if (!item) return; current[targetFolder][draggedFileName] = item; delete current[draggedFileName]; showToast( `Moved "${draggedFileName}" to "${targetFolder}"`, "fa-check-circle" ); draggedFileName = null; if (windows["files"]) { updateFileExplorer(); } } function handleGlobalDragEnd() { document .querySelectorAll(".file-item.drag-over") .forEach((el) => el.classList.remove("drag-over")); draggedFileName = null; } document.addEventListener("dragend", handleGlobalDragEnd); function deleteFile(filename) { const confirmed = confirm(`Are you sure you want to delete "${filename}"?`); if (!confirmed) return; let current = getFileSystemAtPath(currentPath); if (!current || !current[filename]) return; delete current[filename]; showToast(`Deleted: ${filename}`, "fa-trash"); if (selectedFileItem) { selectedFileItem = null; } if (windows["files"]) { updateFileExplorer(); } } document.addEventListener("click", (e) => { if (!e.target.closest(".file-item") && selectedFileItem) { selectedFileItem.classList.remove("selected"); selectedFileItem = null; } }); function expandTreeToPath(path) { let accumulated = []; for (let segment of path) { accumulated.push(segment); const pathString = accumulated.join("/"); const treeItem = document.querySelector( `.file-tree-item[data-path="${pathString}"]` ); const children = document.querySelector( `.file-tree-children[data-path="${pathString}"]` ); if (treeItem && children) { treeItem.classList.add("expanded"); children.classList.add("expanded"); } } } let installedThemes = ["dark"]; let isPasswordless = false; function switchAppStoreSection(section, element) { document .querySelectorAll(".appstore-section") .forEach((s) => s.classList.remove("active")); element.classList.add("active"); const mainContent = document.getElementById("appstoreMain"); if (section === 'community') { mainContent.innerHTML = `

Community

Apps & themes from the community

Loading community repo...

`; fetchCommunityApps(); return; } if (section === "themes") { // Helper for theme items const getTheme = (name, author, desc, key, className) => ({ name: name, author: author, desc: desc, customPreviewHtml: `
`, isInstalled: installedThemes.includes(key), installAction: `installTheme('${key}')`, uninstallAction: `uninstallTheme('${key}')`, type: "theme" }); const items = [ { name: "Dark Theme", author: "PrismX", desc: "The default Nebulo theme. Sleek dark interface with teal accents, perfect for extended use and reducing eye strain.", customPreviewHtml: `
`, isInstalled: true, installButtonText: "Installed (Default)", installButtonDisabled: true, type: "theme" }, getTheme("Light Theme", "PrismX", "A bright and clean theme perfect for daytime use. Easy on the eyes with light backgrounds and dark text.", "light", "illustration-light-theme"), getTheme("Golden Theme", "lanefiedler-731", "Elegant golden accents with warm, luxurious dark backgrounds. Perfect for a premium look.", "golden", "illustration-golden-theme"), getTheme("Red Theme", "lanefiedler-731", "Bold and vibrant red accents for those who want to stand out. Energy meets elegance.", "red", "illustration-red-theme"), getTheme("Blue Theme", "lanefiedler-731", "Cool and calming blue tones. Professional and soothing for extended use.", "blue", "illustration-blue-theme"), getTheme("Purple Theme", "lanefiedler-731", "Deep shades combined with royal hues, crafted together for the perfect purple theme.", "purple", "illustration-purple-theme"), getTheme("Green Theme", "lanefiedler-731", "Rich shades of green with splashes of lime and seaweed, this is quite the exquisite theme.", "green", "illustration-green-theme"), getTheme("Liquid Glass", "$xor", "An Apple insipired theme with a beautiful translucent look.", "liquidGlass", "illustration-liquidglass-theme") ]; mainContent.innerHTML = `

Themes

Customize your Nebulo experience

${items.map(item => renderAppItem(item)).join('')}
`; } else if (section === "apps") { const items = [ { name: "Startup Apps", author: "PrismX", desc: "Control which applications launch automatically on login with this convenient this built-in app.", customPreviewHtml: `
Startup Apps
OFFLINE SUPPORT
`, isInstalled: installedApps.includes("startup-apps"), installAction: "installApp('startup-apps')", uninstallAction: "uninstallApp('startup-apps')", type: "app" }, { name: "Task Manager", author: "PrismX", desc: "Monitor and manage running applications and windows. View system statistics and close unresponsive apps with ease.", customPreviewHtml: `
Task Manager
CPU: 45%
`, isInstalled: installedApps.includes("task-manager"), installAction: "installApp('task-manager')", uninstallAction: "uninstallApp('task-manager')", type: "app" }, { name: "Snap Manager", author: "lanefiedler-731", desc: "Add window snapping with animated previews. Customize layouts, assign shortcuts, and drag to see live guides.", customPreviewHtml: `
`, isInstalled: installedApps.includes("snap-manager"), installAction: "installApp('snap-manager')", uninstallAction: "uninstallApp('snap-manager')", type: "app" }, { name: "Helios", author: "PrismX", desc: "The classic CORS proxy you know and love, fit in to one single file.", customPreviewHtml: `
`, isInstalled: installedApps.includes("helios"), installAction: "installApp('helios')", uninstallAction: "uninstallApp('helios')", type: "app" }, { name: "Visual Studio Code", author: "Microsoft", desc: "The developer's choice for text editing, now on Nebulo.", customPreviewHtml: `
`, isInstalled: installedApps.includes("vsc"), installAction: "installApp('vsc')", uninstallAction: "uninstallApp('vsc')", type: "app" }, { name: "V86 Emulator", author: "lanefiedler-731", desc: "Run x86 operating systems and software within Nebulo. Experience virtualized computing with full system emulation.", customPreviewHtml: `
`, isInstalled: installedApps.includes("v86-emulator"), installAction: "installApp('v86-emulator')", uninstallAction: "uninstallApp('v86-emulator')", type: "app" }, { name: "Nebulo AI Assistant", author: "lanefiedler-731", desc: "Your personal AI assistant powered by WebLLM. Get instant help with Nebulo features, apps, settings, and more. Runs entirely in your browser with no server required!", customPreviewHtml: `
Nebulo AI
`, isInstalled: true, installButtonText: "Open", installAction: "openApp('nebulo-ai')", uninstallAction: "", type: "app" }, { name: "Text Editor", author: "PrismX and Nebulo Labs", desc: "A simple text editor for creating and editing text files.", isInstalled: installedApps.includes("editor"), installAction: "installApp('editor')", uninstallAction: "uninstallApp('editor')", type: "app" }, { name: "Files", author: "PrismX and Nebulo Labs", desc: "File manager for browsing and managing your files.", isInstalled: installedApps.includes("files"), installAction: "installApp('files')", uninstallAction: "uninstallApp('files')", type: "app" }, { name: "About Nebulo", author: "PrismX and Nebulo Labs", desc: "Learn more about Nebulo and its features.", isInstalled: installedApps.includes("about"), installAction: "installApp('about')", uninstallAction: "uninstallApp('about')", type: "app" }, { name: "Settings", author: "PrismX and Nebulo Labs", desc: "Customize your Nebulo experience.", isInstalled: installedApps.includes("settings"), installAction: "installApp('settings')", uninstallAction: "uninstallApp('settings')", type: "app" }, { name: "App Store", author: "PrismX and Nebulo Labs", desc: "Discover and install new apps and themes.", isInstalled: installedApps.includes("appstore"), installAction: "installApp('appstore')", uninstallAction: "uninstallApp('appstore')", type: "app" }, { name: "Calculator", author: "PrismX and Nebulo Labs", desc: "A simple calculator for basic math operations.", isInstalled: installedApps.includes("calculator"), installAction: "installApp('calculator')", uninstallAction: "uninstallApp('calculator')", type: "app" }, { name: "Browser", author: "PrismX and Nebulo Labs", desc: "Browse the web with the built-in Scramjet-powered browser.", isInstalled: installedApps.includes("browser"), installAction: "installApp('browser')", uninstallAction: "uninstallApp('browser')", type: "app" }, { name: "Games", author: "PrismX and Nebulo Labs", desc: "Launch MSN Play in the Scramjet-powered games hub.", isInstalled: installedApps.includes("games"), installAction: "installApp('games')", uninstallAction: "uninstallApp('games')", type: "app" }, { name: "Music", author: "PrismX and Nebulo Labs", desc: "Play and manage your music files.", isInstalled: installedApps.includes("music"), installAction: "installApp('music')", uninstallAction: "uninstallApp('music')", type: "app" }, { name: "Photos", author: "PrismX and Nebulo Labs", desc: "View and manage your photos.", isInstalled: installedApps.includes("photos"), installAction: "installApp('photos')", uninstallAction: "uninstallApp('photos')", type: "app" }, { name: "Terminal", author: "PrismX and Nebulo Labs", desc: "Command line interface for advanced users.", isInstalled: installedApps.includes("terminal"), installAction: "installApp('terminal')", uninstallAction: "uninstallApp('terminal')", type: "app" }, { name: "Python Interpreter", author: "PrismX and Nebulo Labs", desc: "Run Python code interactively.", isInstalled: installedApps.includes("python"), installAction: "installApp('python')", uninstallAction: "uninstallApp('python')", type: "app" }, { name: "Achievements", author: "PrismX and Nebulo Labs", desc: "Track your progress and unlock achievements.", isInstalled: installedApps.includes("achievements"), installAction: "installApp('achievements')", uninstallAction: "uninstallApp('achievements')", type: "app" }, { name: "Credits", author: "PrismX", desc: "View credits and acknowledgments for the developers and contributors behind NautilusOS.", customPreviewHtml: `
Credits
PrismX
Lead Developer
`, isInstalled: installedApps.includes("credits"), installAction: "installApp('credits')", uninstallAction: "uninstallApp('credits')", type: "app" }, { name: "Global Chat", author: "Nebulo Team", desc: "Connect and chat with other NautilusOS users in real-time. Join the community conversation!", icon: "fas fa-comments", customPreviewHtml: `
Global Chat
`, isInstalled: installedApps.includes("global-chat"), installAction: "installApp('global-chat')", uninstallAction: "uninstallApp('global-chat')", type: "app" } ]; mainContent.innerHTML = `

Apps

Discover and install new applications

${items.map(item => renderAppItem(item)).join('')}
`; } else if (section === "games") { const items = [ { name: "Snake", author: "lanefiedler-731", desc: "A classic snake game. Eat food, grow longer, and try to beat your high score without hitting the walls or yourself!", customPreviewHtml: `
${Array(64).fill(0).map((_, i) => `
`).join("")}
`, isInstalled: installedGames.includes("snake"), installAction: "installGame('snake')", uninstallAction: "", // No uninstall needed? Original didn't show uninstall logic for games properly or handled it? // Original: installedGames.includes("snake") ? "openApp('snake')" : "installGame('snake')" // If installed, it shows "Play". installButtonText: installedGames.includes("snake") ? "Play" : "Install", installAction: installedGames.includes("snake") ? "openApp('snake')" : "installGame('snake')", uninstallAction: "", // Original doesn't seem to support uninstalling games? type: "game" }, { name: "2048", author: "PrismX", desc: "Slide tiles to combine numbers and reach 2048! A addictive puzzle game that's easy to learn but hard to master.", customPreviewHtml: `
2
4
8
16
32
64
128
${Array(9).fill('
').join("")}
`, isInstalled: installedGames.includes("2048"), installButtonText: installedGames.includes("2048") ? "Play" : "Install", installAction: installedGames.includes("2048") ? "openApp('2048')" : "installGame('2048')", type: "game" }, { name: "Tic-Tac-Toe", author: "PrismX", desc: "Classic Tic-Tac-Toe against an AI opponent. Can you outsmart the computer and get three in a row?", customPreviewHtml: `
X
O
X
O
`, isInstalled: installedGames.includes("tictactoe"), installButtonText: installedGames.includes("tictactoe") ? "Play" : "Install", installAction: installedGames.includes("tictactoe") ? "openApp('tictactoe')" : "installGame('tictactoe')", type: "game" }, { name: "AI Snake Learning", author: "lanefiedler-731", desc: "Train an AI neural network to play Snake using Deep Q-Learning. Watch it learn and improve with GPU acceleration, customizable concurrency, game speed, and training speed controls.", customPreviewHtml: `
${Array(25).fill(0).map((_, i) => `
`).join("")}
`, isInstalled: true, installButtonText: "Play", installAction: "openApp('ai-snake')", type: "game" } ]; mainContent.innerHTML = `

Games

Play and enjoy games on Nebulo

${items.map(item => renderAppItem(item)).join('')}
`; } } function installTheme(themeName) { if (installedThemes.includes(themeName)) { showToast("Theme already installed", "fa-info-circle"); return; } installedThemes.push(themeName); localStorage.setItem( "nebulo_installedThemes", JSON.stringify(installedThemes) ); showToast("Theme installed! Go to Settings to apply it.", "fa-check-circle"); unlockAchievement("theme-changer"); refreshAppStore(); } function uninstallTheme(themeName) { const index = installedThemes.indexOf(themeName); if (index > -1) { installedThemes.splice(index, 1); localStorage.setItem( "nebulo_installedThemes", JSON.stringify(installedThemes) ); showToast("Theme uninstalled", "fa-trash"); refreshAppStore(); } } const themeDefinitions = { dark: { url: "/style.css" }, light: { url: "/themes/light.css" }, golden: { url: "/themes/golden.css" }, red: { url: "/themes/red.css" }, blue: { url: "/themes/blue.css" }, purple: { url: "/themes/purple.css" }, green: { url: "/themes/green.css" }, liquidGlass: { url: "/themes/lg.css" }, }; function applyTheme(themeName) { const theme = themeDefinitions[themeName]; const themeLink = document.getElementById("themeLink") if (!theme) { console.warn("Theme not found:", themeName); return; } console.log(themeLink) themeLink.href = theme.url localStorage.setItem("nebulo_currentTheme", themeName); appliedThemeName = themeName; } function refreshAppStore() { const activeSection = document.querySelector(".appstore-section.active"); if (!activeSection) return; const sectionText = activeSection.textContent.trim().toLowerCase(); if (sectionText.includes("community")) { switchAppStoreSection("community", activeSection); return; } if (sectionText.includes("themes")) { switchAppStoreSection("themes", activeSection); } else if (sectionText.includes("apps")) { switchAppStoreSection("apps", activeSection); } else if (sectionText.includes("games")) { switchAppStoreSection("games", activeSection); } else if (sectionText.includes("tools")) { switchAppStoreSection("tools", activeSection); } } let occupiedGridCells = new Set(); function handleTaskbarClick(appName) { if (windows[appName]) { const win = windows[appName]; if (win.style.display === "none") { win.style.display = "block"; win.classList.remove("minimized"); } focusWindow(win); focusedWindow = appName; updateTaskbarIndicators(); } else { openApp(appName); } } let audioPlayer = null; let currentMusicFile = null; function loadMusicFile(event) { const file = event.target.files[0]; if (!file) return; if (!file.type.startsWith("audio/")) { showToast("Please select a valid audio file", "fa-exclamation-circle"); return; } audioPlayer = document.getElementById("audioPlayer"); if (!audioPlayer) return; const url = URL.createObjectURL(file); audioPlayer.src = url; currentMusicFile = file.name; const fileName = file.name.replace(/\.[^/.]+$/, ""); document.getElementById("musicTitle").textContent = fileName; document.getElementById("musicArtist").textContent = "Local File"; audioPlayer.addEventListener("loadedmetadata", () => { document.getElementById("totalTime").textContent = formatTime( audioPlayer.duration ); }); audioPlayer.addEventListener("timeupdate", updateProgress); audioPlayer.addEventListener("ended", () => { const playBtn = document.getElementById("playPauseBtn"); playBtn.innerHTML = ''; }); const volumeSlider = document.getElementById("volumeSlider"); audioPlayer.volume = volumeSlider.value / 100; showToast("Music loaded: " + fileName, "fa-music"); } function togglePlayPause() { if (!audioPlayer || !audioPlayer.src) { showToast("Please load a music file first", "fa-info-circle"); return; } const playBtn = document.getElementById("playPauseBtn"); if (audioPlayer.paused) { audioPlayer.play(); playBtn.innerHTML = ''; } else { audioPlayer.pause(); playBtn.innerHTML = ''; } } function updateProgress() { if (!audioPlayer) return; const progress = (audioPlayer.currentTime / audioPlayer.duration) * 100; document.getElementById("progressFill").style.width = progress + "%"; document.getElementById("currentTime").textContent = formatTime( audioPlayer.currentTime ); } function seekMusic(event) { if (!audioPlayer || !audioPlayer.src) return; const progressBar = event.currentTarget; const rect = progressBar.getBoundingClientRect(); const x = event.clientX - rect.left; const percentage = x / rect.width; const newTime = percentage * audioPlayer.duration; audioPlayer.currentTime = newTime; } function changeVolume(value) { if (audioPlayer) { audioPlayer.volume = value / 100; } document.getElementById("volumePercent").textContent = value + "%"; } function skipForward() { if (!audioPlayer || !audioPlayer.src) return; audioPlayer.currentTime = Math.min( audioPlayer.currentTime + 10, audioPlayer.duration ); } function skipBackward() { if (!audioPlayer || !audioPlayer.src) return; audioPlayer.currentTime = Math.max(audioPlayer.currentTime - 10, 0); } function formatTime(seconds) { if (isNaN(seconds)) return "0:00"; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return mins + ":" + (secs < 10 ? "0" : "") + secs; } function restartMusic() { if (!audioPlayer || !audioPlayer.src) { showToast("Please load a music file first", "fa-info-circle"); return; } audioPlayer.currentTime = 0; if (audioPlayer.paused) { audioPlayer.play(); const playBtn = document.getElementById("playPauseBtn"); playBtn.innerHTML = ''; } } function toggleLoop() { if (!audioPlayer || !audioPlayer.src) { showToast("Please load a music file first", "fa-info-circle"); return; } const loopBtn = document.getElementById("loopBtn"); audioPlayer.loop = !audioPlayer.loop; if (audioPlayer.loop) { loopBtn.classList.add("active"); showToast("Loop enabled", "fa-repeat"); } else { loopBtn.classList.remove("active"); showToast("Loop disabled", "fa-repeat"); } } let browserTabs = [ { id: 0, title: "New Tab", url: "", history: [], historyIndex: -1, }, ]; let activeBrowserTab = 0; let browserTabIdCounter = 1; function createBrowserTab() { const newTab = { id: browserTabIdCounter++, title: "New Tab", url: "", history: [], historyIndex: -1, }; browserTabs.push(newTab); const tabsContainer = document.getElementById("browserTabs"); if (!tabsContainer) return; const newTabBtn = tabsContainer.querySelector(".browser-new-tab"); const tabEl = document.createElement("div"); tabEl.className = "browser-tab"; tabEl.dataset.tabId = newTab.id; tabEl.innerHTML = ` New Tab
`; tabEl.addEventListener("click", (e) => { if (!e.target.closest(".browser-tab-close")) { switchBrowserTab(newTab.id); } }); const closeBtn = tabEl.querySelector(".browser-tab-close"); closeBtn.addEventListener("click", (e) => { e.stopPropagation(); closeBrowserTab(newTab.id); }); tabsContainer.insertBefore(tabEl, newTabBtn); const contentContainer = document.getElementById("browserContent"); if (!contentContainer) return; const viewEl = document.createElement("div"); viewEl.className = "browser-view"; viewEl.dataset.viewId = newTab.id; viewEl.innerHTML = `
`; contentContainer.appendChild(viewEl); switchBrowserTab(newTab.id); } function switchBrowserTab(tabId) { activeBrowserTab = tabId; document.querySelectorAll(".browser-tab").forEach((tab) => { tab.classList.remove("active"); if (parseInt(tab.dataset.tabId) === tabId) { tab.classList.add("active"); } }); document.querySelectorAll(".browser-view").forEach((view) => { view.classList.remove("active"); if (parseInt(view.dataset.viewId) === tabId) { view.classList.add("active"); } }); const currentTab = browserTabs.find((t) => t.id === tabId); if (currentTab) { const urlInput = document.getElementById("browserUrlInput"); if (urlInput) urlInput.value = currentTab.url; updateBrowserNavButtons(); } } function closeBrowserTab(tabId) { if (browserTabs.length === 1) { showToast("Cannot close the last tab", "fa-exclamation-circle"); return; } const tabIndex = browserTabs.findIndex((t) => t.id === tabId); if (tabIndex === -1) return; browserTabs.splice(tabIndex, 1); const tabEl = document.querySelector(`.browser-tab[data-tab-id="${tabId}"]`); const viewEl = document.querySelector( `.browser-view[data-view-id="${tabId}"]` ); if (tabEl) tabEl.remove(); if (viewEl) viewEl.remove(); if (activeBrowserTab === tabId) { const newActiveTab = browserTabs[Math.max(0, tabIndex - 1)]; if (newActiveTab) { switchBrowserTab(newActiveTab.id); } } } function handleBrowserUrlInput(event) { if (event.key === "Enter") { const input = event.target; navigateBrowser(input.value); } } function handleBrowserLandingInput(event) { if (event.key === "Enter") { const input = event.target; navigateBrowser(input.value); } } // Transport function moved to index.html to access connection variable // Clean corrupted search engine URLs in localStorage function cleanSearchEngine() { const defaultEngine = 'https://search.brave.com/search?q='; const currentEngine = localStorage.getItem('nOS_searchEngine'); if (!currentEngine || typeof currentEngine !== 'string') { return defaultEngine; } let engine = currentEngine.trim(); if (!engine) { return defaultEngine; } // Unwrap proxied Scramjet URLs if they were accidentally saved. if (isScramjetUrl(engine)) { const decoded = decodeScramjetUrl(engine); if (decoded && decoded !== engine) { engine = decoded; } } else if (engine.includes('/uv/service/') || engine.includes('hvtrs8')) { engine = defaultEngine; } // Normalize to a simple base like "...?q=". const qIndex = engine.indexOf('q='); if (qIndex === -1) { engine = engine.split('?')[0] + '?q='; } else { engine = engine.slice(0, qIndex + 2); const qSeparator = engine[qIndex - 1]; if (qSeparator !== '?' && qSeparator !== '&') { engine = engine.split('?')[0] + '?q='; } } if (engine !== currentEngine) { console.log('Cleaning corrupted search engine from:', currentEngine, 'to:', engine); localStorage.setItem('nOS_searchEngine', engine); } return engine || defaultEngine; } function navigateBrowser(input) { if (!input.trim()) return; let url = input.trim(); // Skip proxy URLs that might be generated accidentally if (isScramjetUrl(url) || url.includes('/uv/service/') || url.includes('hvtrs8') || url.includes('.aoo')) { console.warn('Skipping malformed proxy URL:', url); return; } if (!url.includes(".") || url.includes(" ")) { const searchEngine = cleanSearchEngine() || 'https://search.brave.com/search?q='; // Add language parameter to force English results and prevent localization issues url = searchEngine + encodeURIComponent(url) + '&hl=en&lr=lang_en'; } else { if (!url.startsWith("http://") && !url.startsWith("https://")) { url = "https://" + url; } } const currentTab = browserTabs.find((t) => t.id === activeBrowserTab); if (!currentTab) return; if (currentTab.historyIndex < currentTab.history.length - 1) { currentTab.history = currentTab.history.slice( 0, currentTab.historyIndex + 1 ); } currentTab.history.push(url); currentTab.historyIndex++; currentTab.url = url; const finalUrl = url; const wispInfo = window.currentScramjetWispUrl || SCRAMJET_PRIMARY_WISP; console.log(`[Nebulo] Browser navigation triggered; using WISP ${wispInfo} for ${finalUrl}`); loadBrowserPage(finalUrl); } async function fallbackToExternalProxy(url) { console.log('[DEBUG] Falling back to external proxy for:', url); const currentTab = browserTabs.find((t) => t.id === activeBrowserTab); if (!currentTab) { console.log('[DEBUG] No current tab for external proxy'); return; } const viewEl = document.querySelector( `.browser-view[data-view-id="${activeBrowserTab}"]` ); if (!viewEl) { console.log('[DEBUG] No view element for external proxy'); return; } const urlInput = document.getElementById("browserUrlInput"); if (urlInput) urlInput.value = url; const loading = document.getElementById("browserLoading"); if (loading) loading.classList.add("active"); console.log('[DEBUG] Starting external proxy load, clearing viewEl'); try { viewEl.innerHTML = ""; // Use an iframe instead of direct HTML injection const iframe = document.createElement('iframe'); // Generate proxy URL const proxiedUrl = "https://api.codetabs.com/v1/proxy/?quest=" + encodeURIComponent(url); // Set up iframe with strict sandboxing iframe.src = proxiedUrl; iframe.style.width = '100%'; iframe.style.height = '100%'; iframe.style.border = 'none'; iframe.sandbox = 'allow-forms allow-modals allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-scripts allow-same-origin'; iframe.onload = () => { console.log('[DEBUG] External proxy iframe loaded'); if (loading) loading.classList.remove("active"); }; iframe.onerror = (error) => { console.error('[DEBUG] External proxy iframe failed:', error); // Fallback to error display viewEl.innerHTML = `

Unable to Load Page

The page could not be loaded. Please check the URL and try again.

`; if (loading) loading.classList.remove("active"); }; viewEl.appendChild(iframe); // Update tab title currentTab.title = new URL(url).hostname; const tabEl = document.querySelector( `.browser-tab[data-tab-id="${activeBrowserTab}"]` ); if (tabEl) { const titleEl = tabEl.querySelector(".browser-tab-title"); if (titleEl) titleEl.textContent = currentTab.title; } } catch (error) { console.error("Browser error:", error); viewEl.innerHTML = `

Unable to Load Page

The page could not be loaded. Please check the URL and try again.

`; } finally { if (loading) loading.classList.remove("active"); } } async function loadBrowserPage(url) { console.log('[DEBUG] loadBrowserPage called with URL:', url); const currentTab = browserTabs.find((t) => t.id === activeBrowserTab); if (!currentTab) { console.log('[DEBUG] No current tab found'); return; } const viewEl = document.querySelector( `.browser-view[data-view-id="${activeBrowserTab}"]` ); if (!viewEl) { console.log('[DEBUG] No view element found'); return; } const wispInUse = window.currentScramjetWispUrl || resolvePreferredWispUrl(); console.log(`[Nebulo] Browser load will use WISP ${wispInUse}`); const urlInput = document.getElementById("browserUrlInput"); if (urlInput) urlInput.value = url; const loading = document.getElementById("browserLoading"); if (loading) loading.classList.add("active"); console.log('[DEBUG] Starting to load browser page, clearing viewEl'); try { viewEl.innerHTML = ""; // Check if this is a site that blocks iframes or is problematic. const problematicSites = ["msn.com", "google.com", "bing.com", "yahoo.com", "discord.com", "outlook.com", "microsoft.com"]; const isProblematic = problematicSites.some(site => url.includes(site)); const scramjetController = await initScramjetProxy(); const canUseScramjet = Boolean(scramjetController); if (!canUseScramjet && isProblematic) { console.warn('[DEBUG] Scramjet unavailable for a problematic site, attempting external proxy fallback.'); } console.log('[DEBUG] Problematic site check skipped, proceeding to proxy logic.'); // Try Scramjet first, fallback to external proxy if (canUseScramjet) { console.log('[DEBUG] Using Scramjet proxy for:', url); try { const scramjetUrl = encodeScramjetUrl(url); console.log('[DEBUG] Scramjet encoded URL:', scramjetUrl); console.log('[DEBUG] Creating Scramjet iframe.'); const iframe = document.createElement('iframe'); iframe.src = scramjetUrl; iframe.style.width = '100%'; iframe.style.height = '100%'; iframe.style.border = 'none'; iframe.sandbox = 'allow-forms allow-modals allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts'; console.log('[DEBUG] Created Scramjet iframe with sandbox:', iframe.sandbox); iframe.onload = () => { console.log('[DEBUG] Scramjet iframe loaded successfully'); const loading = document.getElementById("browserLoading"); if (loading) loading.classList.remove("active"); }; iframe.onerror = (error) => { console.warn('[DEBUG] Scramjet iframe failed, falling back to external proxy:', error); iframe.remove(); fallbackToExternalProxy(url); }; console.log('[DEBUG] Appending Scramjet iframe to viewEl'); viewEl.appendChild(iframe); return; } catch (error) { console.warn('[DEBUG] Scramjet failed with error:', error); return fallbackToExternalProxy(url); } } else { console.log('[DEBUG] Scramjet proxy not available or configured, falling back to external proxy for:', url); await fallbackToExternalProxy(url); } const title = new URL(url).hostname; currentTab.title = title; const tabEl = document.querySelector( `.browser-tab[data-tab-id="${activeBrowserTab}"]` ); if (tabEl) { const titleEl = tabEl.querySelector(".browser-tab-title"); if (titleEl) titleEl.textContent = currentTab.title; } viewEl.scrollTop = 0; // For proxy iframe, navigation is handled by the iframe itself // We just need to update the UI when the iframe navigates } catch (error) { console.error("Browser error:", error); viewEl.innerHTML = `

Unable to Load Page

The page could not be loaded. Some websites prevent embedding for security reasons. Try visiting the site directly.

`; } finally { if (loading) loading.classList.remove("active"); } } function browserGoBack() { // Use custom history for external proxy const currentTab = browserTabs.find((t) => t.id === activeBrowserTab); if (!currentTab || currentTab.historyIndex <= 0) return; currentTab.historyIndex--; const url = currentTab.history[currentTab.historyIndex]; currentTab.url = url; loadBrowserPage(url); } function browserGoForward() { // Use custom history for external proxy const currentTab = browserTabs.find((t) => t.id === activeBrowserTab); if (!currentTab || currentTab.historyIndex >= currentTab.history.length - 1) return; currentTab.historyIndex++; const url = currentTab.history[currentTab.historyIndex]; currentTab.url = url; loadBrowserPage(url); } function browserReload() { // Reload for external proxy const currentTab = browserTabs.find((t) => t.id === activeBrowserTab); if (!currentTab || !currentTab.url) { showToast("No page to reload", "fa-info-circle"); return; } loadBrowserPage(currentTab.url); } function updateBrowserNavButtons() { const currentTab = browserTabs.find((t) => t.id === activeBrowserTab); if (!currentTab) return; const backBtn = document.getElementById("browserBack"); const forwardBtn = document.getElementById("browserForward"); if (backBtn) { backBtn.disabled = currentTab.historyIndex <= 0; } if (forwardBtn) { forwardBtn.disabled = currentTab.historyIndex >= currentTab.history.length - 1; } } let calcCurrentValue = "0"; let calcPreviousValue = ""; let calcOperation = ""; let calcShouldResetDisplay = false; function calcInput(value) { const display = document.getElementById("calcDisplay"); const history = document.getElementById("calcHistory"); if (!display) return; if (calcShouldResetDisplay) { calcCurrentValue = ""; calcShouldResetDisplay = false; } if (["+", "-", "*", "/", "%"].includes(value)) { if (calcCurrentValue === "" || calcCurrentValue === "0") return; if (calcPreviousValue !== "" && calcOperation !== "") { calcEquals(); } calcOperation = value; calcPreviousValue = calcCurrentValue; calcCurrentValue = ""; if (history) { const opSymbol = value === "*" ? "×" : value === "/" ? "÷" : value; history.textContent = `${calcPreviousValue} ${opSymbol}`; } display.textContent = "0"; return; } if (value === "." && calcCurrentValue.includes(".")) return; if (calcCurrentValue === "0" && value !== ".") { calcCurrentValue = value; } else { calcCurrentValue += value; } display.textContent = calcCurrentValue; if (history && calcOperation && calcPreviousValue) { const opSymbol = calcOperation === "*" ? "×" : calcOperation === "/" ? "÷" : calcOperation; history.textContent = `${calcPreviousValue} ${opSymbol} ${calcCurrentValue}`; } } function calcEquals() { const display = document.getElementById("calcDisplay"); const history = document.getElementById("calcHistory"); if (!display) return; if ( calcPreviousValue === "" || calcOperation === "" || calcCurrentValue === "" ) return; const prev = parseFloat(calcPreviousValue); const current = parseFloat(calcCurrentValue); let result = 0; switch (calcOperation) { case "+": result = prev + current; break; case "-": result = prev - current; break; case "*": result = prev * current; break; case "/": if (current === 0) { showToast("Cannot divide by zero", "fa-exclamation-circle"); calcClear(); return; } result = prev / current; break; case "%": result = prev % current; break; } result = Math.round(result * 100000000) / 100000000; if (history) { const opSymbol = calcOperation === "*" ? "×" : calcOperation === "/" ? "÷" : calcOperation; history.textContent = `${calcPreviousValue} ${opSymbol} ${calcCurrentValue} =`; } calcCurrentValue = result.toString(); display.textContent = calcCurrentValue; calcPreviousValue = ""; calcOperation = ""; calcShouldResetDisplay = true; } function calcClear() { calcCurrentValue = "0"; calcPreviousValue = ""; calcOperation = ""; calcShouldResetDisplay = false; const display = document.getElementById("calcDisplay"); const history = document.getElementById("calcHistory"); if (display) display.textContent = "0"; if (history) history.textContent = ""; } function calcBackspace() { const display = document.getElementById("calcDisplay"); if (!display) return; if (calcShouldResetDisplay) { calcClear(); return; } if (calcCurrentValue.length > 1) { calcCurrentValue = calcCurrentValue.slice(0, -1); } else { calcCurrentValue = "0"; } display.textContent = calcCurrentValue; } // ========== DEBUGGING FOR BROWSER ISOLATION ========== // Monitor for page location changes let originalLocation = window.location.href; setInterval(() => { if (window.location.href !== originalLocation) { console.error('[DEBUG] Page location changed from', originalLocation, 'to', window.location.href); originalLocation = window.location.href; } }, 100); // Monitor for DOM changes to global chat const globalChatObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'childList' || mutation.type === 'characterData') { const target = mutation.target; if (target.id === 'globalChatWindow' || target.id === 'chatMessages' || target.closest('#globalChatWindow')) { console.log('[DEBUG] Global chat DOM changed:', mutation.type, 'on', target.id || target.className); } } }); }); document.addEventListener('DOMContentLoaded', () => { const chatWindow = document.getElementById('globalChatWindow'); if (chatWindow) { globalChatObserver.observe(chatWindow, { childList: true, subtree: true, characterData: true }); console.log('[DEBUG] Global chat observer attached'); } }); // Monitor for iframe creations const iframeObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.tagName === 'IFRAME') { console.log('[DEBUG] New iframe created:', node.src, 'sandbox:', node.sandbox); } }); }); }); iframeObserver.observe(document.body, { childList: true, subtree: true }); console.log('[DEBUG] Debugging observers initialized'); // ========== PYTHON INTERPRETER FUNCTIONS ========== // Simple Python interpreter using eval (simulated) function runPythonCode() { const editor = document.getElementById("pythonCodeEditor"); const output = document.getElementById("pythonOutput"); if (!editor || !output) { showToast("Python interpreter not ready", "fa-exclamation-circle"); return; } const code = editor.value; output.innerHTML = '
Running...
'; // Simulated Python execution try { let printOutput = []; // Create a simulated Python environment const pythonEnv = { print: (...args) => { printOutput.push(args.map(String).join(' ')); }, len: (arr) => arr.length, sum: (arr) => arr.reduce((a, b) => a + b, 0), range: (start, end, step = 1) => { const arr = []; if (end === undefined) { end = start; start = 0; } for (let i = start; i < end; i += step) { arr.push(i); } return arr; }, abs: Math.abs, max: Math.max, min: Math.min, round: Math.round, str: String, int: parseInt, float: parseFloat, list: (arr) => Array.from(arr), }; // Convert Python-like syntax to JavaScript with proper indentation handling let jsCode = code .replace(/print\(/g, 'pythonEnv.print(') .replace(/len\(/g, 'pythonEnv.len(') .replace(/sum\(/g, 'pythonEnv.sum(') .replace(/range\(/g, 'pythonEnv.range(') .replace(/abs\(/g, 'pythonEnv.abs(') .replace(/max\(/g, 'pythonEnv.max(') .replace(/min\(/g, 'pythonEnv.min(') .replace(/round\(/g, 'pythonEnv.round(') .replace(/#.*$/gm, '//') // Convert comments .replace(/True/g, 'true') // Convert True .replace(/False/g, 'false') // Convert False .replace(/None/g, 'null'); // Convert None // Process indentation-based blocks properly const lines = jsCode.split('\n'); let indentStack = []; let processedLines = []; let inFunction = false; let functionIndent = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmedLine = line.trim(); if (trimmedLine === '' || trimmedLine.startsWith('//')) { processedLines.push(line); continue; } const indent = line.length - line.trimStart().length; const currentIndent = indent; // Handle function definitions if (trimmedLine.match(/^def\s+\w+\s*\(/)) { // Close any previous blocks at this level while (indentStack.length > 0 && indentStack[indentStack.length - 1] >= currentIndent) { indentStack.pop(); processedLines.push(' '.repeat(Math.max(0, indentStack.length > 0 ? indentStack[indentStack.length - 1] : 0)) + '}'); } // Convert function definition const funcMatch = trimmedLine.match(/^def\s+(\w+)\s*\((.*?)\)\s*:/); if (funcMatch) { const funcName = funcMatch[1]; const params = funcMatch[2]; processedLines.push(' '.repeat(currentIndent) + `function ${funcName}(${params}) {`); indentStack.push(currentIndent); inFunction = true; functionIndent = currentIndent; } continue; } // Handle control structures if (trimmedLine.match(/^(if|elif|else|for|while)\b/)) { // Close any previous blocks at this level while (indentStack.length > 0 && indentStack[indentStack.length - 1] >= currentIndent) { indentStack.pop(); processedLines.push(' '.repeat(Math.max(0, indentStack.length > 0 ? indentStack[indentStack.length - 1] : 0)) + '}'); } let convertedLine = trimmedLine; if (trimmedLine.startsWith('if ')) { convertedLine = trimmedLine.replace(/^if\s+(.*?):/, 'if ($1) {'); } else if (trimmedLine.startsWith('elif ')) { convertedLine = trimmedLine.replace(/^elif\s+(.*?):/, '} else if ($1) {'); } else if (trimmedLine.startsWith('else:')) { convertedLine = '} else {'; } else if (trimmedLine.match(/^for\s+\w+\s+in\s+/)) { convertedLine = trimmedLine.replace(/^for\s+(\w+)\s+in\s+(.*?):/, 'for (let $1 of $2) {'); } else if (trimmedLine.startsWith('while ')) { convertedLine = trimmedLine.replace(/^while\s+(.*?):/, 'while ($1) {'); } processedLines.push(' '.repeat(currentIndent) + convertedLine); indentStack.push(currentIndent); continue; } // Handle regular code lines if (indentStack.length > 0) { // Check if we're dedenting while (indentStack.length > 0 && indentStack[indentStack.length - 1] >= currentIndent) { indentStack.pop(); processedLines.push(' '.repeat(Math.max(0, indentStack.length > 0 ? indentStack[indentStack.length - 1] : 0)) + '}'); } } // Add the current line processedLines.push(line); } // Close any remaining blocks while (indentStack.length > 0) { indentStack.pop(); processedLines.push('}'); } jsCode = processedLines.join('\n'); // Execute the code const func = new Function('pythonEnv', jsCode); func(pythonEnv); // Display output if (printOutput.length > 0) { output.innerHTML = printOutput.map(line => `
${escapeHtml(line)}
` ).join(''); } else { output.innerHTML = '
Code executed successfully (no output)
'; } } catch (error) { output.innerHTML = `
Error: ${escapeHtml(error.message)}
`; } } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function clearPythonOutput() { const output = document.getElementById("pythonOutput"); if (output) { output.innerHTML = ''; } } function savePythonFile() { const editor = document.getElementById("pythonCodeEditor"); const filenameInput = document.getElementById("pythonFilename"); if (!editor || !filenameInput) return; const filename = filenameInput.value || 'script.py'; const code = editor.value; // Save to file system const folder = getFileSystemAtPath(["Python"]); if (!folder) { // Create Python folder if it doesn't exist fileSystem.Python = {}; } fileSystem.Python[filename] = code; showToast(`Saved ${filename}`, "fa-check-circle"); } function loadPythonFile() { const filenameInput = document.getElementById("pythonFilename"); if (!filenameInput) return; const filename = filenameInput.value; if (!filename) { showToast("Please enter a filename", "fa-exclamation-circle"); return; } const folder = getFileSystemAtPath(["Python"]); if (!folder || !folder[filename]) { showToast("File not found", "fa-exclamation-circle"); return; } const editor = document.getElementById("pythonCodeEditor"); if (editor) { editor.value = folder[filename]; showToast(`Loaded ${filename}`, "fa-check-circle"); } } // Support opening Python files with double-click function openPythonFile(filename, content) { openApp("python", content, filename); } let notificationHistory = []; function toggleNotificationCenter() { const notif = document.getElementById("notificationCenter"); const quick = document.getElementById("quickActions"); const bell = document.getElementById("notificationBell"); if (quick) quick.classList.remove("active"); notif.classList.toggle("active"); bell.classList.toggle("active"); } function addNotificationToHistory(message, icon = "fa-info-circle") { const notification = { message: message, icon: icon, time: new Date(), id: Date.now(), }; notificationHistory.unshift(notification); if (notificationHistory.length > 50) { notificationHistory = notificationHistory.slice(0, 50); } updateNotificationCenter(); } function updateNotificationCenter() { const listEl = document.getElementById("notificationList"); if (!listEl) return; if (notificationHistory.length === 0) { listEl.innerHTML = `

No notifications

`; return; } listEl.innerHTML = notificationHistory .map((notif) => { const timeAgo = getTimeAgo(notif.time); return `
System
${notif.message}
${timeAgo}
`; }) .join(""); } function getTimeAgo(date) { const seconds = Math.floor((new Date() - date) / 1000); if (seconds < 60) return "Just now"; if (seconds < 3600) return Math.floor(seconds / 60) + "m ago"; if (seconds < 86400) return Math.floor(seconds / 3600) + "h ago"; return Math.floor(seconds / 86400) + "d ago"; } function dismissNotification(id) { notificationHistory = notificationHistory.filter((n) => n.id !== id); updateNotificationCenter(); } function clearAllNotifications() { notificationHistory = []; updateNotificationCenter(); showToast("All notifications cleared", "fa-trash"); } document.addEventListener("click", (e) => { const center = document.getElementById("notificationCenter"); const bell = document.getElementById("notificationBell"); if ( center && bell && !center.contains(e.target) && !bell.contains(e.target) ) { center.classList.remove("active"); bell.classList.remove("active"); } }); function toggleQuickActions() { const menu = document.getElementById("quickActions"); const notif = document.getElementById("notificationCenter"); if (notif) notif.classList.remove("active"); menu.classList.toggle("active"); } function hideQuickActions() { const menu = document.getElementById("quickActions"); menu.classList.remove("active"); } function updatePhotosApp() { if (!windows["photos"]) return; const photos = fileSystem["Photos"] || {}; const photoList = Object.keys(photos); const content = windows["photos"].querySelector(".window-content"); if (!content) return; if (photoList.length === 0) { content.innerHTML = `

No Photos Yet

Take a screenshot to get started!

`; return; } content.innerHTML = `
${photoList .map( (name) => `
${name}
${name}
` ) .join("")}
`; } function viewPhoto(name) { const photos = fileSystem["Photos"] || {}; const url = photos[name]; if (!url) return; const modal = document.createElement("div"); modal.style.cssText = ` position: fixed; inset: 0; background: var(--overlay-darker); z-index: 10004; display: flex; align-items: center; justify-content: center; animation: fadeIn 0.3s ease; `; modal.innerHTML = ` `; modal.onclick = (e) => { if (e.target === modal) modal.remove(); }; document.body.appendChild(modal); } function deletePhoto(name) { const confirmed = confirm(`Delete ${name}?`); if (!confirmed) return; const photos = fileSystem["Photos"] || {}; if (photos[name]) { URL.revokeObjectURL(photos[name]); delete photos[name]; showToast("Photo deleted", "fa-trash"); updatePhotosApp(); } } async function takeScreenshot() { showToast("Taking screenshot...", "fa-camera"); try { const stream = await navigator.mediaDevices.getDisplayMedia({ video: { mediaSource: "screen" }, }); const video = document.createElement("video"); video.srcObject = stream; video.play(); await new Promise((resolve) => { video.onloadedmetadata = resolve; }); const canvas = document.createElement("canvas"); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext("2d"); ctx.drawImage(video, 0, 0); stream.getTracks().forEach((track) => track.stop()); canvas.toBlob((blob) => { const now = new Date(); const month = String(now.getMonth() + 1).padStart(2, "0"); const day = String(now.getDate()).padStart(2, "0"); const year = now.getFullYear(); const hours = String(now.getHours()).padStart(2, "0"); const minutes = String(now.getMinutes()).padStart(2, "0"); const seconds = String(now.getSeconds()).padStart(2, "0"); const filename = `${month}-${day}-${year} ${hours}-${minutes}-${seconds}.png`; const url = URL.createObjectURL(blob); if (!fileSystem["Photos"]) { fileSystem["Photos"] = {}; } fileSystem["Photos"][filename] = url; showToast(`Screenshot saved: ${filename}`, "fa-check-circle"); unlockAchievement("screenshot"); if (!windows["photos"]) { openApp("photos"); } else { updatePhotosApp(); } }, "image/png"); } catch (error) { if (error.name === "NotAllowedError") { showToast("Screenshot cancelled", "fa-info-circle"); } else { showToast("Screenshot failed: " + error.message, "fa-exclamation-circle"); } } } function closeAllWindows() { const windowApps = Object.keys(windows); if (windowApps.length === 0) { showToast("No windows to close", "fa-info-circle"); return; } windowApps.forEach((appName) => { closeWindowByAppName(appName); }); showToast(`Closed ${windowApps.length} window(s)`, "fa-check-circle"); } document.addEventListener("click", (e) => { const menu = document.getElementById("quickActions"); if (menu && !menu.contains(e.target) && !e.target.closest(".taskbar-icon")) { menu.classList.remove("active"); } }); function setupStep1Next() { const username = document.getElementById("setupUsername").value.trim(); const isPasswordless = document.getElementById("setupPasswordless").checked; if (!username) { showToast("Please enter a username", "fa-exclamation-circle"); return; } if (username.length < 3) { showToast( "Username must be at least 3 characters", "fa-exclamation-circle" ); return; } window.setupIsPasswordless = isPasswordless; document.getElementById("setupStep1").style.display = "none"; if (isPasswordless) { document.getElementById("setupStep3").style.display = "block"; } else { document.getElementById("setupStep2").style.display = "block"; } } function setupStep2Back() { document.getElementById("setupStep2").style.display = "none"; document.getElementById("setupStep1").style.display = "block"; const checkbox = document.getElementById("setupPasswordless"); if (checkbox && window.setupIsPasswordless !== undefined) { checkbox.checked = window.setupIsPasswordless; togglePasswordless(); } } function toggleSetupPassword() { const passwordInput = document.getElementById("setupPassword"); const toggleIcon = document.getElementById("setupPasswordToggle"); if (passwordInput.type === "password") { passwordInput.type = "text"; toggleIcon.classList.remove("fa-eye"); toggleIcon.classList.add("fa-eye-slash"); } else { passwordInput.type = "password"; toggleIcon.classList.remove("fa-eye-slash"); toggleIcon.classList.add("fa-eye"); } } function togglePasswordless() { const checkbox = document.getElementById("setupPasswordless"); const passwordLabel = document.getElementById("passwordLabel"); const passwordInput = document.getElementById("setupPassword"); const passwordConfirm = document.getElementById("setupPasswordConfirm"); const passwordToggle = document.getElementById("setupPasswordToggle"); isPasswordless = checkbox.checked; if (isPasswordless) { passwordLabel.textContent = "Passwordless Account"; passwordInput.style.display = "none"; passwordConfirm.style.display = "none"; passwordToggle.style.display = "none"; } else { passwordLabel.textContent = "Create a Password"; passwordInput.style.display = "block"; passwordConfirm.style.display = "block"; passwordToggle.style.display = "block"; } } function setupComplete() { const username = document.getElementById("setupUsername").value.trim(); const isPasswordless = window.setupIsPasswordless || false; const isBloatless = false; const password = isPasswordless ? "" : document.getElementById("setupPassword").value; const appCheckboxes = document.querySelectorAll( '#setupAppOptions input[type="checkbox"]:checked' ); const selectedApps = Array.from(appCheckboxes).map((cb) => cb.value); selectedApps.forEach((app) => { if (!installedApps.includes(app)) { installedApps.push(app); } }); const themeCheckboxes = document.querySelectorAll( '#setupThemeOptions input[type="checkbox"]:checked' ); const selectedThemes = Array.from(themeCheckboxes).map((cb) => cb.value); selectedThemes.forEach((theme) => { if (!installedThemes.includes(theme)) { installedThemes.push(theme); } }); localStorage.setItem("nebulo_username", username); if (isPasswordless) { localStorage.setItem("nebulo_password", ""); localStorage.setItem("nebulo_isPasswordless", "true"); } else { localStorage.setItem("nebulo_password", hashPassword(password)); localStorage.setItem("nebulo_isPasswordless", "false"); } localStorage.setItem("nebulo_setupComplete", "true"); localStorage.setItem("nebulo_bloatlessMode", "false"); localStorage.setItem( "nebulo_installedThemes", JSON.stringify(installedThemes) ); localStorage.setItem( "nebulo_installedApps", JSON.stringify(installedApps) ); localStorage.setItem( "nebulo_startupApps", JSON.stringify(startupApps) ); saveSettingsToLocalStorage(); currentUsername = username; // Set global bloatlessMode and apply filtering immediately bloatlessMode = isBloatless; if (bloatlessMode) { const bloatlessKeep = ["appstore", "settings", "files", "about", "terminal"].filter((app) => !hiddenAppIds.has(app)); const iconsContainer = document.getElementById("desktopIcons"); if (iconsContainer) { const allIcons = iconsContainer.querySelectorAll(".desktop-icon[data-app]"); allIcons.forEach((icon) => { const appName = icon.getAttribute("data-app"); if (!bloatlessKeep.includes(appName) && !installedApps.includes(appName)) { icon.style.display = "none"; } }); } // Also hide from start menu const appGrid = document.querySelector(".app-grid"); if (appGrid) { const allAppItems = appGrid.querySelectorAll(".app-item"); allAppItems.forEach((item) => { const onclickAttr = item.getAttribute("onclick"); if (onclickAttr) { const match = onclickAttr.match(/openApp\(['"]([^'"]+)['"]\)/); if (match) { const appName = match[1]; if (!bloatlessKeep.includes(appName) && !installedApps.includes(appName)) { item.style.display = "none"; } } } }); } // Also hide from taskbar const taskbar = document.querySelector(".taskbar"); if (taskbar) { const allTaskbarIcons = taskbar.querySelectorAll(".taskbar-icon[data-app]"); allTaskbarIcons.forEach((icon) => { const appName = icon.getAttribute("data-app"); if (!bloatlessKeep.includes(appName) && !installedApps.includes(appName)) { icon.style.display = "none"; } }); } } if (selectedThemes.length > 0) { setTimeout(() => { unlockAchievement("theme-changer"); }, 100); } let welcomeMessage = "Setup complete! Welcome to Nebulo"; let toastIcon = "fa-check-circle"; if (username.toLowerCase() === "prismx") { welcomeMessage = "Welcome back, developer! Is it really you?!"; toastIcon = "fa-egg"; } const setup = document.getElementById("setup"); setup.style.opacity = "0"; setTimeout(() => { setup.style.display = "none"; const login = document.getElementById("login"); document.getElementById("username").value = username; updateLoginScreen(); login.classList.add("active"); startLoginClock(); displayBrowserInfo(); updateLoginGreeting(); showToast(welcomeMessage, toastIcon); }, 500); } async function forgotPassword() { const isPasswordless = localStorage.getItem("nebulo_isPasswordless") === "true"; const message = isPasswordless ? "This will reset your passwordless account and return you to setup. All data will be preserved. Continue?" : "This will reset your account and return you to setup. All data will be preserved. Continue?"; const confirmed = await confirm(message); if (!confirmed) return; localStorage.removeItem("nebulo_username"); localStorage.removeItem("nebulo_password"); localStorage.removeItem("nebulo_isPasswordless"); localStorage.removeItem("nebulo_setupComplete"); const usernameInput = document.getElementById("username"); if (usernameInput) usernameInput.value = ""; const passwordInput = document.getElementById("password"); if (passwordInput) passwordInput.value = ""; showToast("Account reset. Reloading...", "fa-info-circle"); setTimeout(() => { location.reload(); }, 1500); } function setupStep2Next() { const password = document.getElementById("setupPassword").value; const passwordConfirm = document.getElementById("setupPasswordConfirm").value; if (!password) { showToast("Please enter a password", "fa-exclamation-circle"); return; } if (password.length < 6) { showToast( "Password must be at least 6 characters", "fa-exclamation-circle" ); return; } if (password !== passwordConfirm) { showToast("Passwords do not match", "fa-exclamation-circle"); return; } document.getElementById("setupStep2").style.display = "none"; document.getElementById("setupStep3").style.display = "block"; } function setupStep3Back() { document.getElementById("setupStep3").style.display = "none"; if (window.setupIsPasswordless) { document.getElementById("setupStep1").style.display = "block"; } else { document.getElementById("setupStep2").style.display = "block"; } } function setupStep3Next() { document.getElementById("setupStep3").style.display = "none"; document.getElementById("setupStep4").style.display = "block"; } function setupStep4Back() { document.getElementById("setupStep4").style.display = "none"; document.getElementById("setupStep3").style.display = "block"; } function setupStep3Back() { document.getElementById("setupStep3").style.display = "none"; document.getElementById("setupStep2").style.display = "block"; } function saveSettingsToLocalStorage() { localStorage.setItem("nebulo_settings", JSON.stringify(settings)); } function loadSettingsFromLocalStorage() { const saved = localStorage.getItem("nebulo_settings"); if (saved) { try { const parsed = JSON.parse(saved); settings = { ...settings, ...parsed }; } catch (e) { console.error("Failed to load settings:", e); } } } function loadAndApplyTheme() { const saved = localStorage.getItem("nebulo_currentTheme"); const themeToApply = (saved && themeDefinitions[saved]) ? saved : "dark"; applyTheme(themeToApply); } function loadInstalledThemes() { const saved = localStorage.getItem("nebulo_installedThemes"); if (saved) { try { installedThemes = JSON.parse(saved); } catch (e) { console.error("Failed to load themes:", e); } } if (!installedThemes.includes("dark")) { installedThemes.unshift("dark"); } } window.addEventListener("DOMContentLoaded", () => { // Initialize default search engine if not set const currentEngine = localStorage.getItem('nOS_searchEngine'); if (!currentEngine || currentEngine.includes('google.com')) { localStorage.setItem('nOS_searchEngine', 'https://search.brave.com/search?q='); console.log('Reset search engine to Brave default'); } loadSnapSettings(); loadSettingsFromLocalStorage(); loadInstalledThemes(); loadAndApplyTheme(); loadInstalledApps(); loadInstalledGames(); loadAchievements(); achievementsData.lastUptimeUpdate = Date.now(); if (!settings.showDesktopIcons) { const icons = document.getElementById("desktopIcons"); if (icons) icons.classList.add("hidden"); } initializeSnapOverlay(); updateSnapOverlayStyles(); document.addEventListener("keydown", handleSnapHotkeys); // Restore Community Apps const savedCommunityApps = JSON.parse(localStorage.getItem('nebulo_communityApps') || '{}'); Object.keys(savedCommunityApps).forEach(appId => { const appData = savedCommunityApps[appId]; createDesktopIcon(appId, appData.name, appData.icon || 'fas fa-box'); }); initAppFolderOverlay(); renderAppFolderLibrary(); // Apply bloatless mode: hide pre-installed desktop icons except App Store, Settings, Files, About, and Terminal if (bloatlessMode) { const bloatlessKeep = ["appstore", "settings", "files", "about", "terminal"].filter((app) => !hiddenAppIds.has(app)); const iconsContainer = document.getElementById("desktopIcons"); if (iconsContainer) { const allIcons = iconsContainer.querySelectorAll(".desktop-icon[data-app]"); allIcons.forEach((icon) => { const appName = icon.getAttribute("data-app"); // Keep only appstore, settings, files, about, terminal, and any user-installed apps/games if (!bloatlessKeep.includes(appName) && !installedApps.includes(appName) && !installedGames.includes(appName)) { icon.style.display = "none"; } }); } } applyUserBackgrounds(); applyProfilePicture(); }); async function signOutToLogin() { const confirmed = await confirm("Are you sure you want to sign out?"); if (!confirmed) return; // Mark current user as offline before signing out if (currentUsername) { markUserOffline(currentUsername); } currentUsername = ""; currentUserAccount = null; updateUsernameBadges(""); updateAccountStatusBadge(""); isSystemLoggedIn = false; const startMenu = document.getElementById("startMenu"); if (startMenu) startMenu.classList.remove("active"); const windowApps = Object.keys(windows); windowApps.forEach((appName) => { const windowEl = windows[appName]; if (windowEl) { windowEl.remove(); } }); windows = {}; focusedWindow = null; const desktop = document.getElementById("desktop"); const login = document.getElementById("login"); desktop.style.opacity = "0"; setTimeout(() => { desktop.classList.remove("active"); desktop.style.opacity = "1"; const password = document.getElementById("password"); if (password) password.value = ""; const username = document.getElementById("username"); const savedUsername = localStorage.getItem("nebulo_username"); if (username && savedUsername) { username.value = savedUsername; } updateLoginScreen(); login.style.display = "flex"; login.style.opacity = "0"; setTimeout(() => { login.classList.add("active"); login.style.opacity = "1"; const isPasswordless = localStorage.getItem("nebulo_isPasswordless") === "true"; if (isPasswordless) { const usernameInput = document.getElementById("username"); if (usernameInput) { setTimeout(() => usernameInput.focus(), 100); } } else if (password) { setTimeout(() => password.focus(), 100); } showToast("Signed out successfully", "fa-sign-out-alt"); }, 50); }, 500); } async function resetAllData() { const confirmed = await confirm( ' WARNING: This will permanently delete ALL your data including:

• Your account (username & password)
• All settings and preferences
• All files and folders
• Installed themes and apps
• Boot preferences

This action CANNOT be undone! Are you absolutely sure you want to continue?' ); if (!confirmed) return; const finalConfirm = await prompt('Type "DELETE" (all caps) to confirm:'); if (finalConfirm !== "DELETE") { showToast("Reset cancelled", "fa-info-circle"); return; } const achievements = localStorage.getItem("nebulo_achievements"); localStorage.clear(); if (achievements) { localStorage.setItem("nebulo_achievements", achievements); } showToast("All data has been erased. Reloading...", "fa-trash-alt"); setTimeout(() => { location.reload(); }, 2000); } async function changeuser() { console.log("Changing user...."); const newUsername = await prompt("Enter a new username:"); if (!newUsername) { showToast("Username change cancelled", "fa-info-circle"); return; } else if (newUsername.length < 3) { showToast("Username must be at least 3 characters", "fa-exclamation-circle"); return } else if (newUsername === currentUsername) { showToast("New username cannot be the same as the current one", "fa-exclamation-circle"); return }; showToast("Username changed successfully. Reloading...", "fa-check-circle"); localStorage.setItem("nebulo_username", newUsername); setTimeout(() => { location.reload(); }, 2000); }; let modalResolve = null; function showModal(options) { return new Promise((resolve) => { modalResolve = resolve; const modal = document.getElementById("customModal"); const icon = document.getElementById("modalIcon"); const title = document.getElementById("modalTitle"); const body = document.getElementById("modalBody"); const buttons = document.getElementById("modalButtons"); const inputContainer = document.getElementById("modalInputContainer"); icon.className = "modal-icon " + (options.type || "info"); icon.innerHTML = ``; title.textContent = options.title || "Confirm"; body.innerHTML = options.message || ""; inputContainer.innerHTML = ""; if (options.prompt) { inputContainer.innerHTML = ``; setTimeout(() => document.getElementById("modalInput").focus(), 100); } const renderButton = (label, clickHandler, className = "modal-btn modal-btn-primary") => { const button = document.createElement("button"); button.className = className; button.type = "button"; button.textContent = label; button.addEventListener("click", clickHandler); buttons.appendChild(button); }; buttons.innerHTML = ""; if (Array.isArray(options.buttons) && options.buttons.length > 0) { options.buttons.forEach((btn) => { const valueToReturn = Object.prototype.hasOwnProperty.call(btn, "value") ? btn.value : btn.label; renderButton( btn.label, () => closeModal(valueToReturn), `modal-btn ${btn.className || "modal-btn-primary"}` ); }); } else if (options.confirm) { renderButton("Cancel", () => closeModal(false), "modal-btn modal-btn-secondary"); renderButton( options.confirmText || "OK", () => confirmModal(), `modal-btn ${options.danger ? "modal-btn-danger" : "modal-btn-primary"}` ); } else { renderButton("OK", () => closeModal(true), "modal-btn modal-btn-primary"); } modal.classList.add("active"); }); } function closeModal(result = false) { const modal = document.getElementById("customModal"); modal.classList.remove("active"); if (modalResolve) { modalResolve(result); modalResolve = null; } } function confirmModal() { const input = document.getElementById("modalInput"); const result = input ? input.value : true; closeModal(result); } function buildProxyEmbedContent(targetUrl, warningText) { return `
${warningText}
`; } window.alert = async (message) => { await showModal({ type: "info", icon: "fa-info-circle", title: "Alert", message: message, confirm: false, }); }; const VERSION_CHECK_INTERVAL_MS = 3 * 60 * 1000; let versionPromptedVersion = localStorage.getItem("nebulo_latestVersionPrompted") || APP_VERSION; async function showVersionUpdatePrompt(latestVersion, releaseNotes = "") { const message = releaseNotes ? `Nebulo ${latestVersion} is live. ${releaseNotes}` : `Nebulo ${latestVersion} is live with fresh updates. Refresh to load the latest build.`; const confirmed = await showModal({ type: "info", icon: "fa-sync-alt", title: "Update available", message, confirm: true, confirmText: "Refresh now", }); if (confirmed) { location.reload(); } } async function checkForVersionUpdate() { try { const response = await fetch(`${VERSION_METADATA_PATH}?t=${Date.now()}`, { cache: "no-store", }); if (!response.ok) { console.warn("[VERSION CHECK] Skipping version check (status not OK)"); return; } const payload = await response.json(); const latestVersion = (payload.version || "").trim(); if (!latestVersion) return; if (latestVersion === APP_VERSION) { if (versionPromptedVersion !== APP_VERSION) { versionPromptedVersion = APP_VERSION; localStorage.setItem("nebulo_latestVersionPrompted", APP_VERSION); } return; } if (versionPromptedVersion === latestVersion) return; versionPromptedVersion = latestVersion; localStorage.setItem("nebulo_latestVersionPrompted", latestVersion); await showVersionUpdatePrompt(latestVersion, payload.notes || ""); } catch (error) { console.warn("[VERSION CHECK] Unable to fetch version metadata:", error); } } setTimeout(checkForVersionUpdate, 2500); setInterval(checkForVersionUpdate, VERSION_CHECK_INTERVAL_MS); window.confirm = async (message) => { return await showModal({ type: "warning", icon: "fa-exclamation-triangle", title: "Confirm", message: message, confirm: true, }); }; window.prompt = async (message, defaultValue = "") => { return await showModal({ type: "info", icon: "fa-question-circle", title: "Input Required", message: message, prompt: true, defaultValue: defaultValue, placeholder: "Enter value...", confirm: true, }); }; let installedApps = []; let startupApps = []; let installedGames = []; let bloatlessMode = false; function hashPassword(password) { const salt = "Nebulo_Salt_2024"; // Simple salt for demo let hash = 0; const combined = password + salt; for (let i = 0; i < combined.length; i++) { const char = combined.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; // Convert to 32-bit integer } return hash.toString(16); } function loadInstalledApps() { const saved = localStorage.getItem("nebulo_installedApps"); if (saved) { try { installedApps = JSON.parse(saved); } catch (e) { console.error("Failed to load apps:", e); } } const savedStartup = localStorage.getItem("nebulo_startupApps"); if (savedStartup) { try { startupApps = JSON.parse(savedStartup); } catch (e) { console.error("Failed to load startup apps:", e); } } installedApps = filterVisibleApps(installedApps); startupApps = filterVisibleApps(startupApps); localStorage.setItem("nebulo_installedApps", JSON.stringify(installedApps)); localStorage.setItem("nebulo_startupApps", JSON.stringify(startupApps)); // Load bloatless mode setting bloatlessMode = localStorage.getItem("nebulo_bloatlessMode") === "true"; } function installApp(appName) { if (hiddenAppIds.has(appName)) { showToast("That app is no longer available.", "fa-ban"); return; } if (installedApps.includes(appName)) { showToast("App already installed", "fa-info-circle"); return; } installedApps.push(appName); localStorage.setItem( "nebulo_installedApps", JSON.stringify(installedApps) ); updateStartMenu(); renderAppFolderLibrary(); if (appName === "snap-manager") { ensureSnapSettingsDefaults(); snapSettings.enabled = true; saveSnapSettings(); initializeSnapOverlay(); updateSnapOverlayStyles(); } // V86 resources will be loaded on-demand when the app is opened showToast( "App installed! Check your desktop and start menu to launch it.", "fa-check-circle" ); refreshAppStore(); } function uninstallApp(appName) { const index = installedApps.indexOf(appName); if (index > -1) { installedApps.splice(index, 1); localStorage.setItem( "nebulo_installedApps", JSON.stringify(installedApps) ); updateStartMenu(); renderAppFolderLibrary(); if (windows[appName]) { closeWindowByAppName(appName); } if (appName === "snap-manager") { ensureSnapSettingsDefaults(); snapSettings.enabled = false; saveSnapSettings(); hideSnapPreview(); } if (appName === "v86-emulator") { // Clean up V86 resources when uninstalling cleanupV86Resources(); } showToast("App uninstalled", "fa-trash"); refreshAppStore(); } } function installGame(gameName) { if (installedGames.includes(gameName)) { showToast("Game already installed", "fa-info-circle"); return; } installedGames.push(gameName); localStorage.setItem( "nebulo_installedGames", JSON.stringify(installedGames) ); updateStartMenu(); renderAppFolderLibrary(); showToast( "Game installed! Check your desktop and start menu to launch it.", "fa-check-circle" ); refreshAppStore(); } function uninstallGame(gameName) { const index = installedGames.indexOf(gameName); if (index > -1) { installedGames.splice(index, 1); localStorage.setItem( "nebulo_installedGames", JSON.stringify(installedGames) ); updateStartMenu(); renderAppFolderLibrary(); if (windows[gameName]) { closeWindowByAppName(gameName); } showToast("Game uninstalled", "fa-trash"); refreshAppStore(); } } function loadInstalledGames() { const saved = localStorage.getItem("nebulo_installedGames"); if (saved) { try { installedGames = JSON.parse(saved); } catch (e) { console.error("Failed to load games:", e); } } } let snakeGame = { canvas: null, ctx: null, snake: [{ x: 10, y: 10 }], food: { x: 15, y: 15 }, direction: { x: 1, y: 0 }, nextDirection: { x: 1, y: 0 }, score: 0, gameRunning: false, gamePaused: false, gameOver: false, gridSize: 20, tileCount: 20, gameSpeed: 100, highScore: localStorage.getItem('snakeHighScore') ? parseInt(localStorage.getItem('snakeHighScore')) : 0, keyListenerAttached: false }; function toggleSnakeGame() { if (!snakeGame.gameRunning && !snakeGame.gameOver) { // Game hasn't started yet startSnakeGame(); } else if (snakeGame.gameRunning && !snakeGame.gamePaused) { // Game is running, pause it snakeGame.gamePaused = true; document.getElementById('snakeStartBtn').textContent = 'Resume'; } else if (snakeGame.gamePaused) { // Game is paused, resume it snakeGame.gamePaused = false; document.getElementById('snakeStartBtn').textContent = 'Pause'; gameLoop(); } } function startSnakeGame() { if (!snakeGame.canvas) { snakeGame.canvas = document.getElementById('snakeCanvas'); snakeGame.ctx = snakeGame.canvas.getContext('2d'); } if (snakeGame.gameRunning && !snakeGame.gamePaused) { return; } if (!snakeGame.gameRunning) { snakeGame.snake = [{ x: 10, y: 10 }]; snakeGame.food = generateFood(); snakeGame.direction = { x: 1, y: 0 }; snakeGame.nextDirection = { x: 1, y: 0 }; snakeGame.score = 0; snakeGame.gameOver = false; snakeGame.gameRunning = true; document.getElementById('snakeScore').textContent = '0'; document.getElementById('snakeStartBtn').textContent = 'Pause'; attachSnakeKeyListeners(); gameLoop(); } else if (snakeGame.gamePaused) { snakeGame.gamePaused = false; document.getElementById('snakeStartBtn').textContent = 'Pause'; gameLoop(); } } function attachSnakeKeyListeners() { if (snakeGame.keyListenerAttached) return; snakeGame.keyListenerAttached = true; document.addEventListener('keydown', handleSnakeKeyPress); } function handleSnakeKeyPress(e) { // Only handle input if snake game is running/paused AND the snake window is focused (highest z-index) if (!snakeGame.gameRunning && !snakeGame.gameOver) return; if (focusedWindow !== "snake") return; if (e.key === ' ') { e.preventDefault(); snakeGame.gamePaused = !snakeGame.gamePaused; document.getElementById('snakeStartBtn').textContent = snakeGame.gamePaused ? 'Resume' : 'Pause'; if (!snakeGame.gamePaused) gameLoop(); return; } if (e.key === 'r' || e.key === 'R') { startSnakeGame(); return; } const key = e.key.toLowerCase(); if (key === 'arrowup' || key === 'w') { if (snakeGame.direction.y === 0) snakeGame.nextDirection = { x: 0, y: -1 }; e.preventDefault(); } else if (key === 'arrowdown' || key === 's') { if (snakeGame.direction.y === 0) snakeGame.nextDirection = { x: 0, y: 1 }; e.preventDefault(); } else if (key === 'arrowleft' || key === 'a') { if (snakeGame.direction.x === 0) snakeGame.nextDirection = { x: -1, y: 0 }; e.preventDefault(); } else if (key === 'arrowright' || key === 'd') { if (snakeGame.direction.x === 0) snakeGame.nextDirection = { x: 1, y: 0 }; e.preventDefault(); } } function generateFood() { let newFood; let foodOnSnake = true; while (foodOnSnake) { newFood = { x: Math.floor(Math.random() * snakeGame.tileCount), y: Math.floor(Math.random() * snakeGame.tileCount) }; foodOnSnake = snakeGame.snake.some(segment => segment.x === newFood.x && segment.y === newFood.y); } return newFood; } function gameLoop() { if (!snakeGame.gameRunning || snakeGame.gamePaused || snakeGame.gameOver) { return; } snakeGame.direction = snakeGame.nextDirection; const head = snakeGame.snake[0]; const newHead = { x: (head.x + snakeGame.direction.x + snakeGame.tileCount) % snakeGame.tileCount, y: (head.y + snakeGame.direction.y + snakeGame.tileCount) % snakeGame.tileCount }; if (snakeGame.snake.some(segment => segment.x === newHead.x && segment.y === newHead.y)) { endSnakeGame(); return; } snakeGame.snake.unshift(newHead); if (newHead.x === snakeGame.food.x && newHead.y === snakeGame.food.y) { snakeGame.score += 10; document.getElementById('snakeScore').textContent = snakeGame.score; snakeGame.food = generateFood(); } else { snakeGame.snake.pop(); } drawSnakeGame(); setTimeout(gameLoop, snakeGame.gameSpeed); } function drawSnakeGame() { const canvas = snakeGame.canvas; const ctx = snakeGame.ctx; if (!canvas || !ctx) return; const bgSecondary = getComputedStyle(document.documentElement).getPropertyValue('--bg-secondary').trim(); const accentColor = getComputedStyle(document.documentElement).getPropertyValue('--accent').trim(); const border = getComputedStyle(document.documentElement).getPropertyValue('--border').trim(); const errorRed = getComputedStyle(document.documentElement).getPropertyValue('--error-red').trim(); ctx.fillStyle = bgSecondary; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.strokeStyle = 'rgba(125, 211, 192, 0.1)'; ctx.lineWidth = 0.5; for (let i = 0; i <= snakeGame.tileCount; i++) { const pos = i * snakeGame.gridSize; ctx.beginPath(); ctx.moveTo(pos, 0); ctx.lineTo(pos, canvas.height); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0, pos); ctx.lineTo(canvas.width, pos); ctx.stroke(); } snakeGame.snake.forEach((segment, index) => { if (index === 0) { ctx.fillStyle = accentColor; } else { ctx.fillStyle = 'rgba(125, 211, 192, 0.7)'; } ctx.fillRect( segment.x * snakeGame.gridSize + 2, segment.y * snakeGame.gridSize + 2, snakeGame.gridSize - 4, snakeGame.gridSize - 4 ); }); ctx.fillStyle = errorRed; ctx.beginPath(); ctx.arc( snakeGame.food.x * snakeGame.gridSize + snakeGame.gridSize / 2, snakeGame.food.y * snakeGame.gridSize + snakeGame.gridSize / 2, snakeGame.gridSize / 2 - 2, 0, 2 * Math.PI ); ctx.fill(); } function endSnakeGame() { snakeGame.gameRunning = false; snakeGame.gameOver = true; if (snakeGame.score > snakeGame.highScore) { snakeGame.highScore = snakeGame.score; localStorage.setItem('snakeHighScore', snakeGame.highScore); document.getElementById('snakeHighScore').textContent = snakeGame.highScore; } document.getElementById('snakeStartBtn').textContent = 'Start Game'; if (snakeGame.keyListenerAttached) { document.removeEventListener('keydown', handleSnakeKeyPress); snakeGame.keyListenerAttached = false; } showToast('Game Over! Score: ' + snakeGame.score + ' | High Score: ' + snakeGame.highScore, 'fa-skull'); } // ========== AI Snake Learning Implementation ========== let aiSnake = { model: null, targetModel: null, canvas: null, ctx: null, games: [], memory: [], isTraining: false, isPaused: false, generation: 0, bestScore: 0, gamesPlayed: 0, scores: [], epsilon: 1.0, epsilonMin: 0.01, epsilonDecay: 0.995, learningRate: 0.001, batchSize: 64, memorySize: 10000, gridSize: 20, tileCount: 20, gameSpeed: 10, trainingSpeed: 10, concurrency: 5, useGPU: true, deviceInfo: null }; // Initialize AI Snake App async function initializeAISnakeApp() { if (typeof tf === 'undefined') { updateAITrainingStatus('Loading TensorFlow.js...'); await new Promise(resolve => setTimeout(resolve, 1000)); if (typeof tf === 'undefined') { updateAITrainingStatus('Error: TensorFlow.js failed to load. Please refresh the page.'); return; } } aiSnake.canvas = document.getElementById('aiSnakeCanvas'); if (!aiSnake.canvas) return; aiSnake.ctx = aiSnake.canvas.getContext('2d'); // Check GPU support try { await tf.ready(); aiSnake.deviceInfo = tf.getBackend(); const useGPU = document.getElementById('aiUseGPU'); if (useGPU) { const useGPUValue = localStorage.getItem('aiSnakeUseGPU'); if (useGPUValue !== null) { useGPU.checked = useGPUValue === 'true'; } aiSnake.useGPU = useGPU.checked; if (aiSnake.useGPU && aiSnake.deviceInfo === 'webgl') { document.getElementById('aiGPUStatus').textContent = 'GPU: WebGL Active'; document.getElementById('aiGPUStatus').style.color = '#4ade80'; } else if (aiSnake.useGPU && aiSnake.deviceInfo === 'webgpu') { document.getElementById('aiGPUStatus').textContent = 'GPU: WebGPU Active'; document.getElementById('aiGPUStatus').style.color = '#4ade80'; } else { document.getElementById('aiGPUStatus').textContent = `GPU: ${aiSnake.deviceInfo} (CPU Fallback)`; document.getElementById('aiGPUStatus').style.color = '#ffaa00'; } } } catch (e) { document.getElementById('aiGPUStatus').textContent = 'GPU: CPU Only'; document.getElementById('aiGPUStatus').style.color = '#ef4444'; } // Load saved state loadAISnakeState(); // Setup UI handlers setupAISnakeUIHandlers(); // Create neural network model await createAISnakeModel(); updateAITrainingStatus('Ready to start training. Click "Start Training" to begin.'); } // Setup UI event handlers function setupAISnakeUIHandlers() { const gameSpeedSlider = document.getElementById('aiGameSpeed'); const trainingSpeedSlider = document.getElementById('aiTrainingSpeed'); const concurrencySlider = document.getElementById('aiConcurrency'); const gpuCheckbox = document.getElementById('aiUseGPU'); if (gameSpeedSlider) { gameSpeedSlider.addEventListener('input', (e) => { aiSnake.gameSpeed = parseInt(e.target.value); document.getElementById('aiGameSpeedValue').textContent = aiSnake.gameSpeed; localStorage.setItem('aiSnakeGameSpeed', aiSnake.gameSpeed); // Update interval if training is running if (aiSnake.isTraining && aiGameInterval) { clearInterval(aiGameInterval); aiGameInterval = setInterval(() => { if (!aiSnake.isPaused) { runAITrainingCycle().catch(err => { console.error('Training error:', err); updateAITrainingStatus('Error during training: ' + err.message); }); } }, aiSnake.gameSpeed); } }); } if (trainingSpeedSlider) { trainingSpeedSlider.addEventListener('input', (e) => { aiSnake.trainingSpeed = parseInt(e.target.value); document.getElementById('aiTrainingSpeedValue').textContent = aiSnake.trainingSpeed; localStorage.setItem('aiSnakeTrainingSpeed', aiSnake.trainingSpeed); }); } if (concurrencySlider) { concurrencySlider.addEventListener('input', (e) => { aiSnake.concurrency = parseInt(e.target.value); document.getElementById('aiConcurrencyValue').textContent = aiSnake.concurrency; localStorage.setItem('aiSnakeConcurrency', aiSnake.concurrency); }); } if (gpuCheckbox) { gpuCheckbox.addEventListener('change', (e) => { aiSnake.useGPU = e.target.checked; localStorage.setItem('aiSnakeUseGPU', aiSnake.useGPU); if (aiSnake.useGPU && aiSnake.deviceInfo === 'webgl') { document.getElementById('aiGPUStatus').textContent = 'GPU: WebGL Active'; document.getElementById('aiGPUStatus').style.color = '#4ade80'; } else { document.getElementById('aiGPUStatus').textContent = `GPU: ${aiSnake.deviceInfo}`; document.getElementById('aiGPUStatus').style.color = '#ffaa00'; } }); } } // Create Deep Q-Network model async function createAISnakeModel() { if (aiSnake.model) { aiSnake.model.dispose(); } if (aiSnake.targetModel) { aiSnake.targetModel.dispose(); } // Input: 24 features (game state) // Output: 4 actions (up, down, left, right) const model = tf.sequential({ layers: [ tf.layers.dense({ inputShape: [24], units: 128, activation: 'relu' }), tf.layers.dense({ units: 128, activation: 'relu' }), tf.layers.dense({ units: 64, activation: 'relu' }), tf.layers.dense({ units: 4, activation: 'linear' }) ] }); model.compile({ optimizer: tf.train.adam(aiSnake.learningRate), loss: 'meanSquaredError' }); aiSnake.model = model; aiSnake.targetModel = model.clone(); updateAITrainingStatus('Neural network model created successfully.'); } // Get game state as feature vector function getGameState(game) { const head = game.snake[0]; const features = []; // Danger straight, right, left const directions = [ game.direction, { x: -game.direction.y, y: game.direction.x }, // right { x: game.direction.y, y: -game.direction.x } // left ]; directions.forEach(dir => { const nextPos = { x: head.x + dir.x, y: head.y + dir.y }; const danger = ( nextPos.x < 0 || nextPos.x >= aiSnake.tileCount || nextPos.y < 0 || nextPos.y >= aiSnake.tileCount || game.snake.some(segment => segment.x === nextPos.x && segment.y === nextPos.y) ) ? 1 : 0; features.push(danger); }); // Current direction (one-hot encoded) features.push(game.direction.x === 0 && game.direction.y === -1 ? 1 : 0); // up features.push(game.direction.x === 0 && game.direction.y === 1 ? 1 : 0); // down features.push(game.direction.x === -1 && game.direction.y === 0 ? 1 : 0); // left features.push(game.direction.x === 1 && game.direction.y === 0 ? 1 : 0); // right // Food direction (normalized) const foodDeltaX = game.food.x - head.x; const foodDeltaY = game.food.y - head.y; features.push(foodDeltaX > 0 ? 1 : (foodDeltaX < 0 ? -1 : 0)); features.push(foodDeltaY > 0 ? 1 : (foodDeltaY < 0 ? -1 : 0)); // Normalized distances features.push(foodDeltaX / aiSnake.tileCount); features.push(foodDeltaY / aiSnake.tileCount); // Snake length (normalized) features.push(game.snake.length / (aiSnake.tileCount * aiSnake.tileCount)); // Additional features: distances to walls features.push(head.x / aiSnake.tileCount); features.push(head.y / aiSnake.tileCount); features.push((aiSnake.tileCount - head.x) / aiSnake.tileCount); features.push((aiSnake.tileCount - head.y) / aiSnake.tileCount); // Body proximity (check immediate surroundings) for (let dx = -1; dx <= 1; dx++) { for (let dy = -1; dy <= 1; dy++) { if (dx === 0 && dy === 0) continue; const checkPos = { x: head.x + dx, y: head.y + dy }; const hasBody = game.snake.some(segment => segment.x === checkPos.x && segment.y === checkPos.y); features.push(hasBody ? 1 : 0); } } return features.slice(0, 24); // Ensure exactly 24 features } // Create a new game instance function createAIGame() { return { snake: [{ x: 10, y: 10 }], food: { x: 15, y: 15 }, direction: { x: 1, y: 0 }, score: 0, steps: 0, gameOver: false, lastFoodDistance: Infinity }; } // Generate food position function generateAIFood(game) { let newFood; let foodOnSnake = true; while (foodOnSnake) { newFood = { x: Math.floor(Math.random() * aiSnake.tileCount), y: Math.floor(Math.random() * aiSnake.tileCount) }; foodOnSnake = game.snake.some(segment => segment.x === newFood.x && segment.y === newFood.y); } return newFood; } // Perform action in game function performAIAction(game, action) { if (game.gameOver) return; // Actions: 0=up, 1=down, 2=left, 3=right const directions = [ { x: 0, y: -1 }, // up { x: 0, y: 1 }, // down { x: -1, y: 0 }, // left { x: 1, y: 0 } // right ]; const newDirection = directions[action]; // Prevent reversing if (newDirection.x === -game.direction.x && newDirection.y === -game.direction.y) { return; } game.direction = newDirection; const head = game.snake[0]; const newHead = { x: (head.x + game.direction.x + aiSnake.tileCount) % aiSnake.tileCount, y: (head.y + game.direction.y + aiSnake.tileCount) % aiSnake.tileCount }; // Check collision if (game.snake.some(segment => segment.x === newHead.x && segment.y === newHead.y)) { game.gameOver = true; return; } game.snake.unshift(newHead); game.steps++; // Check food if (newHead.x === game.food.x && newHead.y === game.food.y) { game.score += 10; game.food = generateAIFood(game); game.lastFoodDistance = Infinity; } else { game.snake.pop(); const foodDist = Math.abs(game.food.x - newHead.x) + Math.abs(game.food.y - newHead.y); game.lastFoodDistance = foodDist; } // Penalty for too many steps without eating if (game.steps > 200 && game.score === 0) { game.gameOver = true; } } // Predict action using model async function predictAction(game) { const state = getGameState(game); const stateTensor = tf.tensor2d([state]); try { const prediction = await aiSnake.model.predict(stateTensor); const qValues = await prediction.data(); prediction.dispose(); stateTensor.dispose(); // Epsilon-greedy exploration if (Math.random() < aiSnake.epsilon) { return Math.floor(Math.random() * 4); } return qValues.indexOf(Math.max(...qValues)); } catch (e) { stateTensor.dispose(); return Math.floor(Math.random() * 4); } } // Train the model async function trainAIModel() { if (aiSnake.memory.length < aiSnake.batchSize) { return; } const batch = []; const sampleIndices = []; for (let i = 0; i < aiSnake.batchSize; i++) { sampleIndices.push(Math.floor(Math.random() * aiSnake.memory.length)); } const states = []; const targets = []; for (const idx of sampleIndices) { const { state, action, reward, nextState, done } = aiSnake.memory[idx]; const stateTensor = tf.tensor2d([state]); const currentQ = await aiSnake.model.predict(stateTensor); const currentQValues = await currentQ.data(); currentQ.dispose(); stateTensor.dispose(); let targetQ = [...currentQValues]; if (done) { targetQ[action] = reward; } else { const nextStateTensor = tf.tensor2d([nextState]); const nextQ = await aiSnake.targetModel.predict(nextStateTensor); const nextQValues = await nextQ.data(); nextQ.dispose(); nextStateTensor.dispose(); targetQ[action] = reward + 0.95 * Math.max(...nextQValues); } states.push(state); targets.push(targetQ); } const xs = tf.tensor2d(states); const ys = tf.tensor2d(targets); await aiSnake.model.fit(xs, ys, { epochs: 1, batchSize: aiSnake.batchSize, verbose: 0 }); xs.dispose(); ys.dispose(); } // Draw game on canvas function drawAISnakeGame(game) { if (!aiSnake.canvas || !aiSnake.ctx) return; const ctx = aiSnake.ctx; const gridSize = aiSnake.canvas.width / aiSnake.tileCount; ctx.fillStyle = '#0f0f0f'; ctx.fillRect(0, 0, aiSnake.canvas.width, aiSnake.canvas.height); // Draw grid ctx.strokeStyle = '#1a3a3a'; ctx.lineWidth = 0.5; for (let i = 0; i <= aiSnake.tileCount; i++) { const pos = i * gridSize; ctx.beginPath(); ctx.moveTo(pos, 0); ctx.lineTo(pos, aiSnake.canvas.height); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0, pos); ctx.lineTo(aiSnake.canvas.width, pos); ctx.stroke(); } // Draw snake game.snake.forEach((segment, index) => { ctx.fillStyle = index === 0 ? '#00ff00' : '#00cc00'; ctx.fillRect( segment.x * gridSize + 1, segment.y * gridSize + 1, gridSize - 2, gridSize - 2 ); }); // Draw food ctx.fillStyle = '#ff4444'; ctx.fillRect( game.food.x * gridSize + 1, game.food.y * gridSize + 1, gridSize - 2, gridSize - 2 ); } // Main training loop let aiTrainingInterval = null; let aiGameInterval = null; async function runAITrainingCycle() { if (!aiSnake.isTraining || aiSnake.isPaused) return; // Update concurrency while (aiSnake.games.length < aiSnake.concurrency) { aiSnake.games.push(createAIGame()); } while (aiSnake.games.length > aiSnake.concurrency) { aiSnake.games.pop(); } // Run games for (let i = 0; i < aiSnake.games.length; i++) { const game = aiSnake.games[i]; if (game.gameOver) { // Store final state const state = getGameState(game); const reward = game.score * 10 - game.steps; aiSnake.memory.push({ state: state, action: 0, // placeholder reward: reward, nextState: state, done: true }); // Update statistics aiSnake.gamesPlayed++; aiSnake.scores.push(game.score); if (game.score > aiSnake.bestScore) { aiSnake.bestScore = game.score; localStorage.setItem('aiSnakeBestScore', aiSnake.bestScore); } // Limit memory size if (aiSnake.memory.length > aiSnake.memorySize) { aiSnake.memory.shift(); } // Create new game aiSnake.games[i] = createAIGame(); } else { // Get action from model const action = await predictAction(game); const state = getGameState(game); const oldScore = game.score; performAIAction(game, action); if (!game.gameOver) { const nextState = getGameState(game); const reward = (game.score > oldScore ? 10 : 0) - 0.1; // small penalty for each step const foodDist = Math.abs(game.food.x - game.snake[0].x) + Math.abs(game.food.y - game.snake[0].y); const oldFoodDist = game.lastFoodDistance || Infinity; const distanceReward = (oldFoodDist - foodDist) * 0.1; aiSnake.memory.push({ state: state, action: action, reward: reward + distanceReward, nextState: nextState, done: false }); // Limit memory size if (aiSnake.memory.length > aiSnake.memorySize) { aiSnake.memory.shift(); } } } } // Draw first game if (aiSnake.games.length > 0) { drawAISnakeGame(aiSnake.games[0]); } // Train model for (let i = 0; i < aiSnake.trainingSpeed; i++) { await trainAIModel(); } // Update target model periodically if (aiSnake.gamesPlayed % 100 === 0 && aiSnake.gamesPlayed > 0) { if (aiSnake.targetModel) { aiSnake.targetModel.dispose(); } aiSnake.targetModel = aiSnake.model.clone(); } // Decay epsilon if (aiSnake.epsilon > aiSnake.epsilonMin) { aiSnake.epsilon *= aiSnake.epsilonDecay; } // Update UI updateAIUI(); } // Update UI elements function updateAIUI() { document.getElementById('aiGeneration').textContent = aiSnake.generation; document.getElementById('aiBestScore').textContent = aiSnake.bestScore; document.getElementById('aiCurrentScore').textContent = aiSnake.games.length > 0 ? aiSnake.games[0].score : 0; document.getElementById('aiGamesPlayed').textContent = aiSnake.gamesPlayed; document.getElementById('aiEpsilon').textContent = aiSnake.epsilon.toFixed(2); if (aiSnake.scores.length > 0) { const avg = aiSnake.scores.slice(-100).reduce((a, b) => a + b, 0) / Math.min(100, aiSnake.scores.length); document.getElementById('aiAvgScore').textContent = avg.toFixed(1); } const status = aiSnake.isTraining ? `Training... Generation ${aiSnake.generation} | Games: ${aiSnake.gamesPlayed} | Best: ${aiSnake.bestScore} | Epsilon: ${aiSnake.epsilon.toFixed(3)}` : 'Training paused'; updateAITrainingStatus(status); } function updateAITrainingStatus(message) { const statusEl = document.getElementById('aiTrainingStatus'); if (statusEl) { statusEl.textContent = message; } } // Start training async function startAISnakeTraining() { if (aiSnake.isTraining) return; if (!aiSnake.model) { await createAISnakeModel(); } aiSnake.isTraining = true; aiSnake.isPaused = false; document.getElementById('aiStartBtn').style.display = 'none'; document.getElementById('aiPauseBtn').style.display = 'block'; // Initialize games aiSnake.games = []; for (let i = 0; i < aiSnake.concurrency; i++) { aiSnake.games.push(createAIGame()); } // Start game loop aiGameInterval = setInterval(() => { if (!aiSnake.isPaused) { runAITrainingCycle().catch(err => { console.error('Training error:', err); updateAITrainingStatus('Error during training: ' + err.message); }); } }, aiSnake.gameSpeed); updateAITrainingStatus('Training started!'); aiSnake.generation++; saveAISnakeState(); } // Pause training function pauseAISnakeTraining() { aiSnake.isPaused = !aiSnake.isPaused; if (aiSnake.isPaused) { document.getElementById('aiPauseBtn').textContent = 'Resume'; updateAITrainingStatus('Training paused.'); } else { document.getElementById('aiPauseBtn').textContent = 'Pause'; updateAITrainingStatus('Training resumed.'); } } // Reset model async function resetAISnakeModel() { if (confirm('Reset AI model? All training progress will be lost.')) { aiSnake.isTraining = false; aiSnake.isPaused = false; if (aiGameInterval) { clearInterval(aiGameInterval); aiGameInterval = null; } document.getElementById('aiStartBtn').style.display = 'block'; document.getElementById('aiPauseBtn').style.display = 'none'; aiSnake.generation = 0; aiSnake.bestScore = 0; aiSnake.gamesPlayed = 0; aiSnake.scores = []; aiSnake.epsilon = 1.0; aiSnake.memory = []; aiSnake.games = []; await createAISnakeModel(); updateAIUI(); updateAITrainingStatus('Model reset. Ready to start training.'); localStorage.removeItem('aiSnakeState'); saveAISnakeState(); } } // Save/Load state function saveAISnakeState() { const state = { generation: aiSnake.generation, bestScore: aiSnake.bestScore, gamesPlayed: aiSnake.gamesPlayed, epsilon: aiSnake.epsilon, gameSpeed: aiSnake.gameSpeed, trainingSpeed: aiSnake.trainingSpeed, concurrency: aiSnake.concurrency }; localStorage.setItem('aiSnakeState', JSON.stringify(state)); } function loadAISnakeState() { const saved = localStorage.getItem('aiSnakeState'); if (saved) { const state = JSON.parse(saved); aiSnake.generation = state.generation || 0; aiSnake.bestScore = parseInt(localStorage.getItem('aiSnakeBestScore')) || 0; aiSnake.gamesPlayed = state.gamesPlayed || 0; aiSnake.epsilon = state.epsilon || 1.0; aiSnake.gameSpeed = parseInt(localStorage.getItem('aiSnakeGameSpeed')) || 10; aiSnake.trainingSpeed = parseInt(localStorage.getItem('aiSnakeTrainingSpeed')) || 10; aiSnake.concurrency = parseInt(localStorage.getItem('aiSnakeConcurrency')) || 5; } // Update UI sliders const gameSpeedSlider = document.getElementById('aiGameSpeed'); const trainingSpeedSlider = document.getElementById('aiTrainingSpeed'); const concurrencySlider = document.getElementById('aiConcurrency'); if (gameSpeedSlider) { gameSpeedSlider.value = aiSnake.gameSpeed; document.getElementById('aiGameSpeedValue').textContent = aiSnake.gameSpeed; } if (trainingSpeedSlider) { trainingSpeedSlider.value = aiSnake.trainingSpeed; document.getElementById('aiTrainingSpeedValue').textContent = aiSnake.trainingSpeed; } if (concurrencySlider) { concurrencySlider.value = aiSnake.concurrency; document.getElementById('aiConcurrencyValue').textContent = aiSnake.concurrency; } } function openStartupApps() { const preinstalledApps = [ { id: "files", name: "Files", icon: "fa-folder" }, { id: "terminal", name: "Terminal", icon: "fa-terminal" }, { id: "browser", name: "Browser", icon: "fa-globe" }, { id: "games", name: "Games", icon: "fa-gamepad" }, { id: "blooket-bot", name: "Blooket Bot", icon: "fa-robot" }, { id: "minecraft", name: "Minecraft", icon: "fa-cube" }, { id: "settings", name: "Settings", icon: "fa-cog" }, { id: "editor", name: "Text Editor", icon: "fa-edit" }, { id: "music", name: "Music", icon: "fa-music" }, { id: "photos", name: "Photos", icon: "fa-images" }, { id: "help", name: "Help", icon: "fa-question-circle" }, { id: "appstore", name: "App Store", icon: "fa-store" }, { id: "calculator", name: "Calculator", icon: "fa-calculator" }, { id: "cloaking", name: "Cloaking", icon: "fa-mask" }, { id: "achievements", name: "Achievements", icon: "fa-trophy" }, { id: "credits", name: "Credits", icon: "fa-heart" }, ].filter((app) => !hiddenAppIds.has(app.id)); const installedAppsData = []; installedApps.forEach((appName) => { if (appName === "startup-apps") { installedAppsData.push({ id: "startup-apps", name: "Startup Apps", icon: "fa-rocket" }); } else if (appName === "task-manager") { installedAppsData.push({ id: "task-manager", name: "Task Manager", icon: "fa-tasks" }); } else if (appName === "snap-manager") { installedAppsData.push({ id: "snap-manager", name: "Snap Manager", icon: "fa-border-all" }); } }); installedGames.forEach((gameName) => { if (gameName === "snake") { installedAppsData.push({ id: "snake", name: "Snake", icon: "fa-gamepad" }); } else if (gameName === "2048") { installedAppsData.push({ id: "2048", name: "2048", icon: "fa-th" }); } else if (gameName === "tictactoe") { installedAppsData.push({ id: "tictactoe", name: "Tic-Tac-Toe", icon: "fa-circle" }); } }); const availableApps = [...preinstalledApps, ...installedAppsData]; const itemsHtml = availableApps .map((app) => { const isEnabled = startupApps.includes(app.id); const isWhatsNew = app.id === "whatsnew"; const disabled = isWhatsNew ? "disabled" : ""; const toggleAction = isWhatsNew ? "" : `onclick="toggleStartupApp('${app.id}')"`; return `
${app.name}
${isWhatsNew ? "Managed in Settings" : isEnabled ? "Enabled" : "Disabled" }
`; }) .join(""); const whatsNewEnabled = localStorage.getItem("nebulo_showWhatsNew") !== "false"; const whatsNewHtml = `
What's New
Managed in Settings
`; const content = `

 Startup Applications

Select which applications should automatically open when you log in.

${itemsHtml} ${whatsNewHtml}
`; createWindow( "Startup Apps", "fas fa-rocket", content, 600, 500, "startup-apps", true ); } function toggleStartupApp(appId) { const index = startupApps.indexOf(appId); if (index > -1) { startupApps.splice(index, 1); } else { startupApps.push(appId); } localStorage.setItem("nebulo_startupApps", JSON.stringify(startupApps)); if (windows["startup-apps"]) { const content = windows["startup-apps"].querySelector(".window-content"); if (content) { const preinstalledApps = [ { id: "files", name: "Files", icon: "fa-folder" }, { id: "terminal", name: "Terminal", icon: "fa-terminal" }, { id: "browser", name: "Browser", icon: "fa-globe" }, { id: "games", name: "Games", icon: "fa-gamepad" }, { id: "blooket-bot", name: "Blooket Bot", icon: "fa-robot" }, { id: "minecraft", name: "Minecraft", icon: "fa-cube" }, { id: "settings", name: "Settings", icon: "fa-cog" }, { id: "editor", name: "Text Editor", icon: "fa-edit" }, { id: "music", name: "Music", icon: "fa-music" }, { id: "photos", name: "Photos", icon: "fa-images" }, { id: "help", name: "Help", icon: "fa-question-circle" }, { id: "appstore", name: "App Store", icon: "fa-store" }, { id: "calculator", name: "Calculator", icon: "fa-calculator" }, { id: "cloaking", name: "Cloaking", icon: "fa-mask" }, { id: "achievements", name: "Achievements", icon: "fa-trophy" }, ].filter((app) => !hiddenAppIds.has(app.id)); const installedAppsData = []; installedApps.forEach((appName) => { if (appName === "startup-apps") { installedAppsData.push({ id: "startup-apps", name: "Startup Apps", icon: "fa-rocket" }); } else if (appName === "task-manager") { installedAppsData.push({ id: "task-manager", name: "Task Manager", icon: "fa-tasks" }); } }); const availableApps = [...preinstalledApps, ...installedAppsData]; const itemsHtml = availableApps .map((app) => { const isEnabled = startupApps.includes(app.id); return `
${app.name }
${isEnabled ? "Enabled" : "Disabled" }
`; }) .join(""); const whatsNewEnabled = localStorage.getItem("nebulo_showWhatsNew") !== "false"; const whatsNewHtml = `
What's New
Managed in Settings
`; content.innerHTML = `

Startup Applications

Select which applications should automatically open when you log in.

${itemsHtml} ${whatsNewHtml}
`; } } } function openTaskManager() { const openWindows = Object.keys(windows); const windowCount = openWindows.length; const processesHtml = openWindows .map((appName) => { const win = windows[appName]; const icon = win.dataset.appIcon || "fa-window-maximize"; const title = win.querySelector(".window-title span").textContent; return `
${title}
Window • Running
`; }) .join(""); const content = `
Open Windows
${windowCount}
System Status
Running

Running Applications

${windowCount === 0 ? '

No applications running

' : processesHtml }

Quick Actions

Refresh
`; createWindow( "Task Manager", "fas fa-tasks", content, 700, 550, "task-manager", true ); } function refreshTaskManager() { if (!windows["task-manager"]) return; const openWindows = Object.keys(windows).filter((w) => w !== "task-manager"); const windowCount = openWindows.length; const processesHtml = openWindows .map((appName) => { const win = windows[appName]; const icon = win.dataset.appIcon || "fa-window-maximize"; const title = win.querySelector(".window-title span").textContent; return `
${title}
Window • Running
`; }) .join(""); const content = windows["task-manager"].querySelector(".window-content"); if (content) { content.innerHTML = `
Open Windows
${windowCount}
System Status
Running

Running Applications

${windowCount === 0 ? '

No applications running

' : processesHtml }

Quick Actions

`; } } function refreshAllApps() { const openWindows = Object.keys(windows).filter((w) => w !== "task-manager"); if (openWindows.length === 0) { showToast("No applications to refresh", "fa-info-circle"); refreshTaskManager(); return; } const appsToReopen = [...openWindows]; showToast("Refreshing all applications...", "fa-sync"); appsToReopen.forEach((appName) => { const windowEl = windows[appName]; if (windowEl) { windowEl.remove(); delete windows[appName]; } }); focusedWindow = null; updateTaskbarIndicators(); refreshTaskManager(); setTimeout(() => { appsToReopen.forEach((appName, index) => { setTimeout(() => { openApp(appName); }, index * 200); }); setTimeout(() => { showToast( `Refreshed ${appsToReopen.length} application(s)`, "fa-check-circle" ); refreshTaskManager(); }, appsToReopen.length * 200 + 500); }, 500); } function launchStartupApps() { setTimeout(() => { startupApps.forEach((appId) => { openApp(appId); }); }, 1000); } function openCredits() { const content = `

Credits

NautilusOS Contributors & Acknowledgments

Dinguschan
Original Template Creator
The original creator of NautilusOS
GitHub

Special Thanks

Open Source Libraries:

  • Font Awesome - Icon library
  • Open Props - CSS framework
  • Monaco Editor - Code editor
  • Prism.js - Syntax highlighting

Contributors:

  • Nebulo Labs - Additional development
  • Community contributors and testers
Made with ❤️ by PrismX
`; createWindow('Credits', 'fa-heart', content, 700, 600); } let backgroundChatContainer = null; let backgroundChatIframe = null; function getChatIframeSrc() { return `./pages/chat-only.html?apiBase=${encodeURIComponent(window.CHAT_API_BASE)}&username=${encodeURIComponent(localStorage.getItem('nebulo_username') || 'User')}`; } function sendChatInitToIframe(iframe) { if (!iframe || !iframe.contentWindow) { return false; } if (iframe.dataset.chatInitSent === 'true') { return true; } iframe.dataset.chatInitSent = 'true'; iframe.contentWindow.postMessage({ type: 'initChat', username: currentUsername, theme: 'nautilus', chatApiBase: window.CHAT_API_BASE }, '*'); return true; } function ensureBackgroundChatIframe() { if (!backgroundChatContainer) { backgroundChatContainer = document.getElementById('backgroundChatContainer'); if (!backgroundChatContainer) { backgroundChatContainer = document.createElement('div'); backgroundChatContainer.id = 'backgroundChatContainer'; backgroundChatContainer.style.cssText = 'position:fixed; width:1px; height:1px; right:0; bottom:0; opacity:0; pointer-events:none; overflow:hidden; z-index:-1;'; document.body.appendChild(backgroundChatContainer); } } if (!backgroundChatIframe) { backgroundChatIframe = document.createElement('iframe'); backgroundChatIframe.id = 'chat-background-iframe'; backgroundChatIframe.src = getChatIframeSrc(); backgroundChatIframe.style.border = 'none'; backgroundChatIframe.style.width = '100%'; backgroundChatIframe.style.height = '100%'; backgroundChatIframe.style.background = 'transparent'; backgroundChatIframe.addEventListener('load', () => { sendChatInitToIframe(backgroundChatIframe); }); backgroundChatContainer.appendChild(backgroundChatIframe); } return backgroundChatIframe; } function attachChatIframeToWindow(container) { const iframe = ensureBackgroundChatIframe(); if (!container || !iframe) { return null; } iframe.id = 'chat-iframe'; iframe.style.width = '100%'; iframe.style.height = '100%'; iframe.style.display = 'block'; iframe.style.background = 'transparent'; container.innerHTML = ''; container.appendChild(iframe); sendChatInitToIframe(iframe); return iframe; } function detachChatIframeToBackground() { if (!backgroundChatIframe || !backgroundChatContainer) { return; } backgroundChatIframe.id = 'chat-background-iframe'; backgroundChatIframe.style.width = '100%'; backgroundChatIframe.style.height = '100%'; if (backgroundChatIframe.parentElement !== backgroundChatContainer) { backgroundChatContainer.appendChild(backgroundChatIframe); } } function initBackgroundChatIframe() { if (!document.body) { return; } ensureBackgroundChatIframe(); } function openGlobalChatWindow() { console.log('🗣️ openGlobalChat() called'); // Check if chat window is already open if (windows['global-chat']) { console.log('📱 Chat window already exists, focusing...'); focusWindow(windows['global-chat']); return; } const appName = 'global-chat'; const title = 'Global Chat'; const icon = 'fas fa-comments'; const width = 900; const height = 700; const windowMargin = 16; const maxW = Math.max(240, window.innerWidth - windowMargin * 2); const maxH = Math.max(180, window.innerHeight - windowMargin * 2); const windowWidth = Math.min(width, maxW); const windowHeight = Math.min(height, maxH); const clamp = (value, min, max) => Math.max(min, Math.min(max, value)); const windowEl = document.createElement("div"); windowEl.className = "window"; windowEl.style.width = windowWidth + "px"; windowEl.style.height = windowHeight + "px"; const baseLeft = window.innerWidth / 2 - windowWidth / 2 + Math.random() * 50; const baseTop = window.innerHeight / 2 - windowHeight / 2 - 30 + Math.random() * 20; const maxLeft = Math.max(windowMargin, window.innerWidth - windowWidth - windowMargin); const maxTop = Math.max(windowMargin, window.innerHeight - windowHeight - windowMargin); windowEl.style.left = clamp(baseLeft, windowMargin, maxLeft) + "px"; windowEl.style.top = clamp(baseTop, windowMargin, maxTop) + "px"; windowEl.style.zIndex = ++zIndexCounter; const chatContent = `
`; windowEl.innerHTML = `
${title}
${chatContent}
`; windowEl.dataset.appIcon = icon; windowEl.dataset.appName = appName; document.getElementById("desktop").appendChild(windowEl); const chatContainer = windowEl.querySelector('#global-chat-container'); const chatIframe = attachChatIframeToWindow(chatContainer); if (chatIframe && chatIframe.contentWindow) { chatIframe.contentWindow.postMessage({ type: 'chatOpened' }, '*'); } window.chatWindowOpen = true; clearChatPingsForCurrentUser(); makeDraggable(windowEl); makeResizable(windowEl); windows[appName] = windowEl; focusedWindow = appName; addDynamicTaskbarIcon(appName, icon); windowEl.addEventListener("mousedown", () => { focusWindow(windowEl); focusedWindow = appName; updateTaskbarIndicators(); }); updateTaskbarIndicators(); // Chat iframe initializes on load. } // Update the global chat button badge with unread count function updateGlobalChatBadge(countOrShow = 0) { const badge = document.getElementById('chatNotificationBadge'); if (!badge) { console.error('Chat notification badge element not found!'); return; } let count = 0; let show = false; if (typeof countOrShow === 'number') { count = Math.max(0, Math.floor(countOrShow)); show = count > 0; } else { show = countOrShow !== false; } if (show) { const displayCount = count > 99 ? '99+' : (count > 0 ? String(count) : ''); badge.textContent = displayCount; badge.style.display = 'flex'; badge.style.visibility = 'visible'; badge.style.opacity = '1'; badge.style.zIndex = '1000'; badge.style.pointerEvents = 'none'; } else { badge.textContent = ''; badge.style.display = 'none'; badge.style.visibility = 'hidden'; badge.style.opacity = '0'; } } // Make function globally accessible for testing window.updateGlobalChatBadge = updateGlobalChatBadge; // Test function for debugging window.testChatBadge = function(count = 5) { console.log('🧪 Testing chat badge with count:', count); updateGlobalChatBadge(count); // Also test if badge element exists const badge = document.getElementById('chatNotificationBadge'); console.log('Badge element:', badge); if (badge) { console.log('Badge position:', badge.offsetTop, badge.offsetLeft); console.log('Badge visibility:', badge.style.display); console.log('Badge content:', badge.textContent); // Force show for testing badge.style.display = 'flex'; badge.style.visibility = 'visible'; badge.style.opacity = '1'; console.log('✅ Forced badge to show for testing'); } }; // Test notification system window.testChatNotification = function(username = 'TestUser', message = 'This is a test message') { console.log('🧪 Testing chat notification system'); showChatToast(username, message); }; // Test cross-tab account updates window.testCrossTabUpdate = function() { console.log('🧪 Testing cross-tab account updates...'); // Manually trigger the storage event simulation const event = new StorageEvent('storage', { key: 'nebulo_accounts', oldValue: null, newValue: localStorage.getItem('nebulo_accounts'), storageArea: localStorage }); window.dispatchEvent(event); console.log('✅ Simulated storage event dispatched'); }; // Check all localStorage keys and data window.debugLocalStorage = function() { console.log('🔍 All localStorage keys and values:'); for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); const value = localStorage.getItem(key); console.log(`${key}: ${value.substring(0, 100)}${value.length > 100 ? '...' : ''}`); } // Specifically check accounts const accountsKey = 'nebulo_accounts'; const accountsData = localStorage.getItem(accountsKey); console.log(`\n📊 Accounts key "${accountsKey}":`, accountsData); if (accountsData) { try { const accounts = JSON.parse(accountsData); console.log(`📊 Parsed ${accounts.length} accounts:`, accounts.map(acc => ({ username: acc.username, isOnline: acc.isOnline, lastOnline: acc.lastOnline }))); } catch (e) { console.error('❌ Error parsing accounts:', e); } } else { console.log('❌ No accounts data found'); } }; // Test adding a fake online user window.testAddOnlineUser = function(username = 'TestUser') { console.log('🧪 Adding test online user:', username); const accounts = getAllAccounts(); console.log('Current accounts before:', accounts.map(acc => ({username: acc.username, isOnline: acc.isOnline, lastOnline: acc.lastOnline}))); // Check if user already exists let existingUser = accounts.find(acc => acc.username === username); if (!existingUser) { // Create a test account const testAccount = new UserAccount(username, '', 'standard', true); accounts.push(testAccount); existingUser = testAccount; console.log('📝 Created new test account for:', username); } else { console.log('📝 Found existing account for:', username); } // Mark as online existingUser.lastOnline = Date.now(); existingUser.isOnline = true; console.log('🔄 Marked user as online:', username, 'at', existingUser.lastOnline); saveAllAccounts(accounts); console.log('✅ Added/updated test user:', username); const onlineUsers = getOnlineUsers(); console.log('Current online users:', onlineUsers.map(u => u.username)); console.log('All accounts after:', accounts.map(acc => ({username: acc.username, isOnline: acc.isOnline, lastOnline: acc.lastOnline}))); }; // Test creating multiple accounts window.testCreateMultipleAccounts = function() { console.log('🧪 Creating multiple test accounts...'); const testUsers = ['Alice', 'Bob', 'Charlie', 'Diana']; testUsers.forEach(username => { const result = createAccount(username, '', 'standard', true); console.log(`Account creation for ${username}:`, result); }); // Mark some as online const accounts = getAllAccounts(); accounts.forEach(account => { if (Math.random() > 0.5) { // Randomly mark half as online account.isOnline = true; account.lastOnline = Date.now(); } }); saveAllAccounts(accounts); console.log('✅ Created test accounts. Check localStorage:'); debugLocalStorage(); }; // Test online users system window.testOnlineUsers = function() { console.log('🧪 Testing online users system'); const accounts = getAllAccounts(); console.log('All accounts:', accounts); console.log('Current username:', currentUsername); console.log('Current user account:', currentUserAccount); const onlineUsers = getOnlineUsers(); // Add avatar URLs to each user const usersWithAvatars = onlineUsers.map(user => ({ ...user, avatar: getUserProfilePicture(user.username) })); console.log('Current online users with avatars:', usersWithAvatars); console.log('Current user profile picture:', getUserProfilePicture(currentUsername)); // First, let's see what iframes are on the page const allIframes = document.querySelectorAll('iframe'); console.log('🔍 All iframes on page:', allIframes.length); allIframes.forEach((iframe, i) => { console.log(` Iframe ${i}: src="${iframe.src}", parent:`, iframe.parentElement?.className); }); // Test sending online users to chat - try multiple selectors let foundIframe = false; // Try different selectors const selectors = [ '#global-chat-container iframe', '.window iframe[src*="chat-only.html"]', 'iframe[src*="chat-only.html"]', '.window-content iframe', '.window iframe' ]; for (const selector of selectors) { const iframes = document.querySelectorAll(selector); if (iframes.length > 0) { console.log(`✅ Found ${iframes.length} iframe(s) with selector "${selector}", sending online users with avatars...`); iframes.forEach(iframe => { if (iframe.contentWindow) { iframe.contentWindow.postMessage({ type: 'onlineUsers', users: usersWithAvatars }, '*'); console.log('✅ Sent online users with avatars to iframe'); } else { console.log('❌ Iframe found but no contentWindow'); } }); foundIframe = true; break; } } if (!foundIframe) { console.log('❌ No chat iframe found. Checking all windows...'); // Debug: show all windows and iframes const allWindows = document.querySelectorAll('.window'); console.log('All windows:', allWindows.length); allWindows.forEach((win, i) => { console.log(`Window ${i}:`, win.className, win.dataset?.app); const iframes = win.querySelectorAll('iframe'); console.log(` Iframes in window ${i}:`, iframes.length); }); const allIframes = document.querySelectorAll('iframe'); console.log('All iframes on page:', allIframes.length); allIframes.forEach((iframe, i) => { console.log(`Iframe ${i} src:`, iframe.src); }); } }; // Show chat notification toast function showChatToast(username, content) { const normalizedName = (username || '').trim(); const displayName = normalizedName.toLowerCase() === 'shs12lord' ? 'Owner' : (normalizedName || 'User'); showToast(`dY'? ${displayName}: ${content}`, 'fa-comments'); } function initChatIframe(iframeEl) { const iframe = iframeEl || document.querySelector('#global-chat-container iframe') || document.getElementById('chat-background-iframe'); if (!iframe) { console.log('Chat iframe not found'); return false; } if (!iframe.contentWindow) { console.log('Chat iframe contentWindow not available'); return false; } // Send the initial chatNotificationsEnabled state to the iframe iframe.contentWindow.postMessage({ type: 'toggleNotifications', enabled: chatNotificationsEnabled }, '*'); console.log('Sent initial chatNotificationsEnabled state to iframe:', chatNotificationsEnabled); try { if (!sendChatInitToIframe(iframe)) { return false; } // Notify chat that it was opened (to reset unread count and mark as initialized) setTimeout(() => { if (iframe.contentWindow) { iframe.contentWindow.postMessage({ type: 'chatOpened' }, '*'); console.log('Chat opened notification sent'); } }, 1000); console.log('Chat init message sent successfully'); return true; } catch (error) { console.log('Chat iframe initialization failed:', error); return false; } } const _originalOpenApp = openApp; window.openApp = openApp = function (appName, ...args) { if (appName === "startup-apps") { openStartupApps(); } else if (appName === "task-manager") { openTaskManager(); } else if (appName === "credits") { openCredits(); } else if (appName === "global-chat") { openGlobalChat(); } else { _originalOpenApp(appName, ...args); } setTimeout(() => { if (windows["task-manager"]) { refreshTaskManager(); } }, 100); }; const _originalCloseWindow = closeWindow; window.closeWindow = closeWindow = function (btn, appName) { _originalCloseWindow(btn, appName); // Track chat window state if (appName === 'global-chat') { window.chatWindowOpen = false; const iframe = backgroundChatIframe || document.querySelector('#global-chat-container iframe') || document.getElementById('chat-background-iframe'); if (iframe && iframe.contentWindow) { iframe.contentWindow.postMessage({ type: 'chatClosed' }, '*'); console.log('Chat closed notification sent to iframe'); } detachChatIframeToBackground(); } setTimeout(() => { if (windows["task-manager"]) { refreshTaskManager(); } }, 150); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initBackgroundChatIframe); } else { initBackgroundChatIframe(); } function initStartMenuSearch() { const searchInput = document.getElementById('startSearch'); if (!searchInput || searchInput.dataset.listenerAdded) { return; } searchInput.dataset.listenerAdded = 'true'; searchInput.addEventListener('input', function () { const searchTerm = this.value.toLowerCase(); const appItems = document.querySelectorAll('.folder-app-item'); appItems.forEach(item => { const appName = item.textContent.toLowerCase(); if (appName.includes(searchTerm)) { item.style.display = ''; } else { item.style.display = 'none'; } }); }); } const essentialStartApps = new Set(["browser", "games", "blooket-bot", "minecraft"]); function getFolderAppList() { const folderSet = new Set(); Object.keys(appMetadata).forEach((appName) => { if (!isVisibleApp(appName)) return; if (essentialStartApps.has(appName)) return; folderSet.add(appName); }); installedApps.forEach((appName) => { if (!isVisibleApp(appName)) return; if (essentialStartApps.has(appName)) return; folderSet.add(appName); }); installedGames.forEach((gameName) => { if (!isVisibleApp(gameName)) return; if (essentialStartApps.has(gameName)) return; folderSet.add(gameName); }); return Array.from(folderSet); } function updateStartMenu() { const folderGrid = document.querySelector(".folder-grid"); if (!folderGrid) return; folderGrid.innerHTML = ""; const appsToShow = getFolderAppList(); appsToShow.forEach((appName) => { const metadata = appMetadata[appName]; if (!metadata) return; const appItem = document.createElement("div"); appItem.className = "folder-app-item"; appItem.onclick = () => { toggleAppFolder(false); openApp(appName); }; const iconMarkup = metadata.iconImage ? `${metadata.name}` : ``; appItem.innerHTML = ` ${iconMarkup} ${metadata.name} `; folderGrid.appendChild(appItem); }); initStartMenuSearch(); } async function exportProfile() { const profile = { version: "1.0", username: localStorage.getItem("nebulo_username"), password: localStorage.getItem("nebulo_password"), isPasswordless: localStorage.getItem("nebulo_isPasswordless") === "true", bloatlessMode: localStorage.getItem("nebulo_bloatlessMode") === "true", settings: settings, installedThemes: installedThemes, installedApps: installedApps, startupApps: startupApps, fileSystem: fileSystem, showWhatsNew: localStorage.getItem("nebulo_showWhatsNew"), exportDate: new Date().toISOString(), }; const wallpaper = localStorage.getItem("nebulo_wallpaper"); const loginWallpaper = localStorage.getItem("nebulo_loginBackground"); const useSame = localStorage.getItem("nebulo_useSameBackground"); const profilePicture = localStorage.getItem("nebulo_profilePicture"); profile.wallpaper = wallpaper || null; profile.loginWallpaper = loginWallpaper || null; profile.useSameBackground = useSame === null ? "true" : useSame; profile.profilePicture = profilePicture || null; const profileJson = await encryption.encrypt(profile); const blob = new Blob([profileJson], { type: "application/json" }); const url = URL.createObjectURL(blob); const username = currentUsername || "user"; const date = new Date().toISOString().split("T")[0]; const filename = `Nebulo_${username}_${date}.nautilusprofile`; const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); showToast("Profile exported successfully!", "fa-check-circle"); } // ========== ACCOUNT MANAGER FUNCTIONS ========== function openAccountManager() { const accounts = getAllAccounts(); const accountListHTML = accounts.map(acc => ` `).join(''); showModal({ type: "info", icon: "fa-users-cog", title: "Account Management", message: `
${accountListHTML}
`, confirm: false }); } function openCreateAccountDialog() { showModal({ type: "info", icon: "fa-user-plus", title: "Create New Account", message: `
`, confirm: true, confirmText: "Create Account", cancelText: "Cancel" }).then(result => { if (result) { const username = document.getElementById("newAccountUsername").value; const password = document.getElementById("newAccountPassword").value; const isPasswordless = document.getElementById("newAccountPasswordless").checked; const isSuperUser = document.getElementById("newAccountSuperUser").checked; if (!username) { showToast("Username is required", "fa-exclamation-circle"); return; } if (!isPasswordless && !password) { showToast("Password is required for non-passwordless accounts", "fa-exclamation-circle"); return; } const result = createAccount(username, password, isSuperUser ? "superuser" : "standard", isPasswordless); if (result.success) { showToast(result.message, "fa-check-circle"); openAccountManager(); } else { showToast(result.message, "fa-exclamation-circle"); } } }); } function editAccountPermissions(username) { const account = getAccountByUsername(username); if (!account) { showToast("Account not found", "fa-exclamation-circle"); return; } // Get all available apps const allApps = Object.keys(appMetadata); const allowedApps = account.allowedApps || []; const appCheckboxes = allApps.map(appKey => { const app = appMetadata[appKey]; const isAllowed = allowedApps.length === 0 || allowedApps.includes(appKey); return ` `; }).join(''); showModal({ type: "info", icon: "fa-key", title: `Permissions for ${username}`, message: `

Select which apps this user can access. If none are selected, all apps are allowed.

${appCheckboxes}
`, confirm: true, confirmText: "Save Permissions", cancelText: "Cancel" }).then(result => { if (result) { const checkboxes = document.querySelectorAll('.app-permission-checkbox'); const selectedApps = Array.from(checkboxes) .filter(cb => cb.checked) .map(cb => cb.value); // If all apps are selected, set to empty array (meaning all allowed) const updatedAllowedApps = selectedApps.length === allApps.length ? [] : selectedApps; updateAccount(username, { allowedApps: updatedAllowedApps }); showToast(`Permissions updated for ${username}`, "fa-check-circle"); // If updating current user, reload their account if (username === currentUsername) { currentUserAccount = getAccountByUsername(username); } } }); } function deleteAccountConfirm(username) { showModal({ type: "warning", icon: "fa-exclamation-triangle", title: "Delete Account", message: `Are you sure you want to delete the account "${username}"? This action cannot be undone.`, confirm: true, confirmText: "Delete", cancelText: "Cancel" }).then(result => { if (result) { const delResult = deleteAccount(username); if (delResult.success) { showToast(delResult.message, "fa-check-circle"); openAccountManager(); } else { showToast(delResult.message, "fa-exclamation-circle"); } } }); } async function importProfile(event) { const file = event.target.files[0]; if (!file) return; if (!file.name.endsWith(".nautilusprofile")) { showToast( "Invalid file format. Please select a .nautilusprofile file.", "fa-exclamation-circle" ); return; } const reader = new FileReader(); reader.onload = async function (e) { try { const profile = await encryption.decrypt(e.target.result); if (!profile.version || !profile.username) { throw new Error("Invalid profile format"); } const confirmed = await confirm( `Import profile for user "${profile.username}"?

` + `This will replace your current settings and data.

` + `Profile Details:
` + `• Username: ${profile.username}
` + `• Exported: ${new Date( profile.exportDate ).toLocaleDateString()}
` + `• Installed Apps: ${(profile.installedApps || []).length}
` + `• Installed Themes: ${(profile.installedThemes || []).length}` ); if (!confirmed) { showToast("Profile import cancelled", "fa-info-circle"); return; } localStorage.setItem("nebulo_username", profile.username); localStorage.setItem("nebulo_password", profile.password || ""); const isPasswordless = profile.isPasswordless !== undefined ? String(profile.isPasswordless) : (profile.password === "" ? "true" : "false"); localStorage.setItem("nebulo_isPasswordless", isPasswordless); // Restore bloatless mode setting bloatlessMode = profile.bloatlessMode || false; localStorage.setItem("nebulo_bloatlessMode", bloatlessMode ? "true" : "false"); localStorage.setItem("nebulo_setupComplete", "true"); settings = profile.settings || settings; installedThemes = profile.installedThemes || []; installedApps = profile.installedApps || []; startupApps = profile.startupApps || []; localStorage.setItem( "nebulo_settings", JSON.stringify(settings) ); localStorage.setItem( "nebulo_installedThemes", JSON.stringify(installedThemes) ); localStorage.setItem( "nebulo_installedApps", JSON.stringify(installedApps) ); localStorage.setItem( "nebulo_startupApps", JSON.stringify(startupApps) ); if (profile.useSameBackground !== null && profile.useSameBackground !== undefined) { localStorage.setItem( "nebulo_useSameBackground", String(profile.useSameBackground) ); } else { localStorage.removeItem("nebulo_useSameBackground"); } if (profile.wallpaper) { localStorage.setItem("nebulo_wallpaper", profile.wallpaper); } else { localStorage.removeItem("nebulo_wallpaper"); } if (profile.loginWallpaper) { localStorage.setItem( "nebulo_loginBackground", profile.loginWallpaper ); } else { localStorage.removeItem("nebulo_loginBackground"); } if (profile.profilePicture) { localStorage.setItem("nebulo_profilePicture", profile.profilePicture); } else { localStorage.removeItem("nebulo_profilePicture"); } if (profile.showWhatsNew !== null && profile.showWhatsNew !== undefined) { localStorage.setItem("nebulo_showWhatsNew", profile.showWhatsNew); } if (profile.fileSystem) { const cleanedFileSystem = { ...profile.fileSystem }; if (cleanedFileSystem.Photos) { cleanedFileSystem.Photos = {}; } fileSystem = cleanedFileSystem; } checkImportedAchievements(); applyUserBackgrounds(); applyProfilePicture(); initializeAppearanceSettings(); showToast( "Profile imported successfully! Redirecting to login...", "fa-check-circle" ); setTimeout(() => { const setup = document.getElementById("setup"); setup.style.opacity = "0"; setTimeout(() => { setup.style.display = "none"; const login = document.getElementById("login"); login.classList.add("active"); document.getElementById("username").value = profile.username; updateLoginScreen(); startLoginClock(); displayBrowserInfo(); updateLoginGreeting(); }, 500); }, 1500); } catch (error) { console.error("Import error:", error); showToast( "Failed to import profile. File may be corrupted or invalid.", "fa-exclamation-circle" ); } }; reader.onerror = function () { showToast("Failed to read profile file.", "fa-exclamation-circle"); }; reader.readAsText(file); } const DEFAULT_CLOAK_TITLE = "Google Docs"; const DEFAULT_CLOAK_FAVICON_URL = "https://google-docs.en.softonic.com/"; let cloakingConfig = { autoRotate: false, rotateSpeed: 10, rotationList: [ { title: "Google", url: "https://www.google.com" }, { title: "Gmail", url: "https://mail.google.com" }, { title: "Google Drive", url: "https://drive.google.com" }, ], currentRotationIndex: 0, panicKeyEnabled: false, panicKey: "Escape", panicUrl: "https://classroom.google.com", antiScreenMonitoring: true, antiMonitorDelay: 1000, useAntiMonitorDelay: false, confirmPageClosing: true, cloakActive: false, cloakTitle: DEFAULT_CLOAK_TITLE, cloakFaviconUrl: DEFAULT_CLOAK_FAVICON_URL, }; let rotationInterval = null; const originalTitle = document.title; let achievementsData = { achievements: { "first-login": { id: "first-login", name: "Welcome Aboard!", description: "Successfully log in to Nebulo for the first time", icon: "fa-fish", unlocked: false, unlockedDate: null, }, "uptime-1h": { id: "uptime-1h", name: "Time Traveler", description: "Keep Nebulo running for 1 hour of total uptime", icon: "fa-clock", unlocked: false, unlockedDate: null, progress: 0, target: 60, }, "uptime-5h": { id: "uptime-5h", name: "Marathon Runner", description: "Achieve 5 hours of total uptime across all sessions", icon: "fa-clock", unlocked: false, unlockedDate: null, progress: 0, target: 300, }, "uptime-10h": { id: "uptime-10h", name: "Dedicated", description: "Reach an impressive 10 hours of total uptime", icon: "fa-clock", unlocked: false, unlockedDate: null, progress: 0, target: 600, }, "uptime-24h": { id: "uptime-24h", name: "Please Go Outside", description: "Achieve the ultimate milestone: 24 hours of total uptime!", icon: "fa-clock", unlocked: false, unlockedDate: null, progress: 0, target: 1440, }, "all-apps": { id: "all-apps", name: "Explorer", description: "Open all preinstalled applications at least once", icon: "fa-compass", unlocked: false, unlockedDate: null, progress: 0, target: 12, }, "stealth-mode": { id: "stealth-mode", name: "Stealth Mode", description: "Apply any cloaking setting to disguise your tab", icon: "fa-user-secret", unlocked: false, unlockedDate: null, }, screenshot: { id: "screenshot", name: "Shutterbug", description: "Take your first screenshot", icon: "fa-camera", unlocked: false, unlockedDate: null, }, "theme-changer": { id: "theme-changer", name: "Style Master", description: "Install a theme from the App Store", icon: "fa-palette", unlocked: false, unlockedDate: null, }, "file-creator": { id: "file-creator", name: "Content Creator", description: "Create and save 5 text files", icon: "fa-file-alt", unlocked: false, unlockedDate: null, progress: 0, target: 5, }, multitasker: { id: "multitasker", name: "Multitasker", description: "Have 5 windows open simultaneously", icon: "fa-layer-group", unlocked: false, unlockedDate: null, }, customizer: { id: "customizer", name: "Customizer", description: "Change at least 3 different settings", icon: "fa-sliders-h", unlocked: false, unlockedDate: null, progress: 0, target: 3, }, }, easterEggs: { "dev-mode": { id: "dev-mode", name: "The Creator", lockedName: "???", description: "Welcome, developer! Are you sure it's really you?", icon: "fa-code", unlocked: false, unlockedDate: null, hint: "Try logging in with a special username...", }, "konami-code": { id: "konami-code", name: "Old School Gamer", lockedName: "???", description: "You entered the legendary Konami Code!", icon: "fa-gamepad", unlocked: false, unlockedDate: null, hint: "Up, Up, Down, Down, Left, Right... what's next?", }, "night-owl": { id: "night-owl", name: "Night Owl", lockedName: "???", description: "You used Nebulo at an ungodly hour (12-3 AM)", icon: "fa-moon", unlocked: false, unlockedDate: null, hint: "Login during the witching hours...", }, "speed-demon": { id: "speed-demon", name: "Speed Demon", lockedName: "???", description: "You opened 10 windows in under 30 seconds! How did you even manage this??", icon: "fa-bolt", unlocked: false, unlockedDate: null, hint: "Open many windows very quickly...", trackingData: { windowOpenTimes: [], threshold: 10, timeWindow: 30000, }, }, }, openedApps: new Set(), settingsChanged: new Set(), totalUptime: 0, lastUptimeUpdate: Date.now(), }; function loadAchievements() { const saved = localStorage.getItem("nebulo_achievements"); if (saved) { try { const parsed = JSON.parse(saved); achievementsData.achievements = { ...achievementsData.achievements, ...parsed.achievements, }; achievementsData.easterEggs = { ...achievementsData.easterEggs, ...parsed.easterEggs, }; achievementsData.openedApps = new Set(parsed.openedApps || []); achievementsData.settingsChanged = new Set(parsed.settingsChanged || []); achievementsData.totalUptime = parsed.totalUptime || 0; } catch (e) { console.error("Failed to load achievements:", e); } } } function saveAchievements() { const toSave = { achievements: achievementsData.achievements, easterEggs: achievementsData.easterEggs, openedApps: Array.from(achievementsData.openedApps), settingsChanged: Array.from(achievementsData.settingsChanged), totalUptime: achievementsData.totalUptime, }; localStorage.setItem("nebulo_achievements", JSON.stringify(toSave)); } function unlockAchievement(achievementId) { const achievement = achievementsData.achievements[achievementId]; if (!achievement || achievement.unlocked) return; achievement.unlocked = true; achievement.unlockedDate = new Date().toISOString(); saveAchievements(); showToast(`Achievement Unlocked: ${achievement.name}!`, "fa-trophy"); if (windows["achievements"]) { refreshAchievementsWindow(); } } function unlockEasterEgg(eggId) { const egg = achievementsData.easterEggs[eggId]; if (!egg || egg.unlocked) return; egg.unlocked = true; egg.unlockedDate = new Date().toISOString(); saveAchievements(); showToast(`Easter Egg Found: ${egg.name}!`, "fa-egg"); if (windows["achievements"]) { refreshAchievementsWindow(); } } function refreshAchievementsWindow() { if (!windows["achievements"]) return; const content = windows["achievements"].querySelector(".window-content"); if (!content) return; const unlockedCount = Object.values(achievementsData.achievements).filter( (a) => a.unlocked ).length; const totalCount = Object.keys(achievementsData.achievements).length; const easterEggsCount = Object.values(achievementsData.easterEggs).filter( (e) => e.unlocked ).length; const totalEggs = Object.keys(achievementsData.easterEggs).length; const achievementsHtml = Object.values(achievementsData.achievements) .map((achievement) => { const hasProgress = achievement.target !== undefined; const progressPercent = hasProgress ? ((achievement.progress / achievement.target) * 100).toFixed(0) : 0; const progressText = hasProgress ? `${achievement.progress}/${achievement.target}` : ""; return `
${achievement.unlocked ? '
' : "" }
${achievement.unlocked ? achievement.name : "???" }
${achievement.unlocked ? achievement.description : "Locked - Keep exploring to unlock!" }
${hasProgress && !achievement.unlocked ? `
${progressText}
` : "" } ${achievement.unlocked ? `
Unlocked: ${new Date( achievement.unlockedDate ).toLocaleDateString()}
` : "" }
`; }) .join(""); const easterEggsHtml = Object.values(achievementsData.easterEggs) .map((egg) => { const displayName = egg.unlocked ? egg.name : egg.lockedName || "???"; const displayDesc = egg.unlocked ? egg.description : egg.hint; return `
${displayName}
${egg.unlocked ? displayDesc : `${displayDesc}` }
${egg.unlocked ? `
Found: ${new Date( egg.unlockedDate ).toLocaleDateString()}
` : "" }
`; }) .join(""); content.innerHTML = `

Achievements

${unlockedCount}/${totalCount}
Achievements
${easterEggsCount}/${totalEggs}
Easter Eggs
${Math.floor( achievementsData.totalUptime )}m
Total Uptime

Your Achievements

${achievementsHtml}

Easter Eggs

${easterEggsHtml}
`; } function trackAppOpened(appName) { const preinstalledApps = [ "files", "terminal", "browser", "games", "blooket-bot", "minecraft", "settings", "editor", "music", "photos", "help", "whatsnew", "appstore", "calculator", "cloaking", ].filter((app) => !hiddenAppIds.has(app)); if (preinstalledApps.includes(appName)) { achievementsData.openedApps.add(appName); const achievement = achievementsData.achievements["all-apps"]; achievement.progress = achievementsData.openedApps.size; if (achievementsData.openedApps.size >= achievement.target) { unlockAchievement("all-apps"); } saveAchievements(); } } function trackSettingChanged(settingName) { achievementsData.settingsChanged.add(settingName); const achievement = achievementsData.achievements["customizer"]; achievement.progress = achievementsData.settingsChanged.size; if (achievementsData.settingsChanged.size >= achievement.target) { unlockAchievement("customizer"); } saveAchievements(); } function updateUptime() { const now = Date.now(); const elapsed = (now - achievementsData.lastUptimeUpdate) / 1000 / 60; achievementsData.totalUptime += elapsed; achievementsData.lastUptimeUpdate = now; const uptime1h = achievementsData.achievements["uptime-1h"]; uptime1h.progress = Math.min(achievementsData.totalUptime, uptime1h.target); if (achievementsData.totalUptime >= uptime1h.target) { unlockAchievement("uptime-1h"); } const uptime5h = achievementsData.achievements["uptime-5h"]; uptime5h.progress = Math.min(achievementsData.totalUptime, uptime5h.target); if (achievementsData.totalUptime >= uptime5h.target) { unlockAchievement("uptime-5h"); } saveAchievements(); } function openAchievements() { const unlockedCount = Object.values(achievementsData.achievements).filter( (a) => a.unlocked ).length; const totalCount = Object.keys(achievementsData.achievements).length; const easterEggsCount = Object.values(achievementsData.easterEggs).filter( (e) => e.unlocked ).length; const totalEggs = Object.keys(achievementsData.easterEggs).length; const achievementsHtml = Object.values(achievementsData.achievements) .map((achievement) => { const hasProgress = achievement.target !== undefined; const progressPercent = hasProgress ? ((achievement.progress / achievement.target) * 100).toFixed(0) : 0; const progressText = hasProgress ? `${achievement.progress}/${achievement.target}` : ""; return `
${achievement.unlocked ? '
' : "" }
${achievement.unlocked ? achievement.name : "???" }
${achievement.unlocked ? achievement.description : "Locked - Keep exploring to unlock!" }
${hasProgress && !achievement.unlocked ? `
${progressText}
` : "" } ${achievement.unlocked ? `
Unlocked: ${new Date( achievement.unlockedDate ).toLocaleDateString()}
` : "" }
`; }) .join(""); const easterEggsHtml = Object.values(achievementsData.easterEggs) .map((egg) => { const displayName = egg.unlocked ? egg.name : egg.lockedName || "???"; const displayDesc = egg.unlocked ? egg.description : egg.hint; return `
${displayName}
${egg.unlocked ? displayDesc : `${displayDesc}` }
${egg.unlocked ? `
Found: ${new Date( egg.unlockedDate ).toLocaleDateString()}
` : "" }
`; }) .join(""); const content = `

Achievements

${unlockedCount}/${totalCount}
Achievements
${easterEggsCount}/${totalEggs}
Easter Eggs
${Math.floor( achievementsData.totalUptime )}m
Total Uptime

Your Achievements

${achievementsHtml}

Easter Eggs

${easterEggsHtml}
`; createWindow( "Achievements", "fas fa-trophy", content, 800, 600, "achievements", true ); } let konamiCode = []; const konamiSequence = [ "ArrowUp", "ArrowUp", "ArrowDown", "ArrowDown", "ArrowLeft", "ArrowRight", "ArrowLeft", "ArrowRight", "KeyB", "KeyA", ]; document.addEventListener("keydown", (e) => { konamiCode.push(e.code); if (konamiCode.length > konamiSequence.length) { konamiCode.shift(); } if (JSON.stringify(konamiCode) === JSON.stringify(konamiSequence)) { unlockEasterEgg("konami-code"); konamiCode = []; } }); function checkNightOwl() { const hour = new Date().getHours(); if (hour >= 0 && hour < 3) { unlockEasterEgg("night-owl"); } } const originalFavicon = document.querySelector('link[rel="icon"]')?.href || ""; function loadCloakingConfig() { const saved = localStorage.getItem("nebulo_cloaking"); if (saved) { try { const parsed = JSON.parse(saved); cloakingConfig = { ...cloakingConfig, ...parsed, }; cloakingConfig.panicKeyEnabled = parsed.panicKeyEnabled ?? false; cloakingConfig.panicKey = parsed.panicKey ?? "Escape"; cloakingConfig.panicUrl = parsed.panicUrl ?? "https://classroom.google.com"; cloakingConfig.cloakActive = parsed.cloakActive ?? cloakingConfig.cloakActive; cloakingConfig.cloakTitle = parsed.cloakTitle ?? cloakingConfig.cloakTitle; cloakingConfig.cloakFaviconUrl = parsed.cloakFaviconUrl ?? cloakingConfig.cloakFaviconUrl; } catch (e) { console.error("Failed to load cloaking config:", e); } } if (cloakingConfig.panicKeyEnabled) { setupPanicKeyListener(); } } function saveCloakingConfig() { localStorage.setItem("nebulo_cloaking", JSON.stringify(cloakingConfig)); } function applyCloaking() { const titleInput = document.getElementById("cloakTitle"); const faviconInput = document.getElementById("cloakFavicon"); const title = titleInput?.value.trim(); const faviconUrl = faviconInput?.value.trim(); if (!title && !faviconUrl) { showToast("Please enter a title or favicon URL", "fa-exclamation-circle"); return; } const finalTitle = title || cloakingConfig.cloakTitle || DEFAULT_CLOAK_TITLE; const finalFaviconUrl = faviconUrl || cloakingConfig.cloakFaviconUrl || DEFAULT_CLOAK_FAVICON_URL; applyCloakState(finalTitle, finalFaviconUrl); cloakingConfig.cloakActive = true; cloakingConfig.cloakTitle = finalTitle; cloakingConfig.cloakFaviconUrl = finalFaviconUrl; saveCloakingConfig(); if (titleInput) titleInput.value = finalTitle; if (faviconInput) faviconInput.value = finalFaviconUrl; showToast("Cloaking applied!", "fa-check-circle"); unlockAchievement("stealth-mode"); updateCloakPreview(); } function setFavicon(url) { const existingFavicons = document.querySelectorAll('link[rel="icon"]'); existingFavicons.forEach((favicon) => favicon.remove()); let faviconLink = document.createElement("link"); faviconLink.rel = "icon"; faviconLink.type = "image/x-icon"; let domain = url; try { const urlObj = new URL(url.startsWith("http") ? url : "https://" + url); domain = urlObj.origin; } catch (e) { domain = "https://" + url.replace(/^https?:\/\//, ""); } faviconLink.href = `https://www.google.com/s2/favicons?domain=${domain}&sz=64`; document.head.appendChild(faviconLink); faviconLink.onerror = () => { faviconLink.href = `https://icons.duckduckgo.com/ip3/${domain.replace( /^https?:\/\//, "" )}.ico`; }; } function applyCloakState(title, faviconUrl) { if (title) { document.title = title; } if (faviconUrl) { setFavicon(faviconUrl); } } function resetCloaking() { document.title = originalTitle; const existingFavicons = document.querySelectorAll('link[rel="icon"]'); existingFavicons.forEach((favicon) => favicon.remove()); if (originalFavicon) { const faviconLink = document.createElement("link"); faviconLink.rel = "icon"; faviconLink.href = originalFavicon; document.head.appendChild(faviconLink); } const titleInput = document.getElementById("cloakTitle"); const faviconInput = document.getElementById("cloakFavicon"); if (titleInput) titleInput.value = DEFAULT_CLOAK_TITLE; if (faviconInput) faviconInput.value = DEFAULT_CLOAK_FAVICON_URL; cloakingConfig.cloakActive = false; cloakingConfig.cloakTitle = DEFAULT_CLOAK_TITLE; cloakingConfig.cloakFaviconUrl = DEFAULT_CLOAK_FAVICON_URL; saveCloakingConfig(); updateCloakPreview(); showToast("Cloaking reset to default", "fa-undo"); } function toggleAutoRotate() { cloakingConfig.autoRotate = !cloakingConfig.autoRotate; const toggle = document.getElementById("autoRotateToggle"); const settings = document.getElementById("rotateSettings"); const indicator = document.querySelector( '.cloaking-tab[data-tab="rotate"] .cloaking-status-indicator' ); const statusDesc = document.querySelector( '.cloaking-tab[data-tab="rotate"] .cloaking-status-desc' ); if (cloakingConfig.autoRotate) { if (toggle) toggle.classList.add("active"); if (settings) { settings.style.opacity = "1"; settings.style.pointerEvents = "all"; } if (indicator) indicator.classList.add("active"); if (statusDesc) statusDesc.textContent = "Currently Active"; startRotation(); showToast("Auto-rotate enabled", "fa-sync-alt"); } else { if (toggle) toggle.classList.remove("active"); if (settings) { settings.style.opacity = "0.5"; settings.style.pointerEvents = "none"; } if (indicator) indicator.classList.remove("active"); if (statusDesc) statusDesc.textContent = "Currently Inactive"; stopRotation(); showToast("Auto-rotate disabled", "fa-sync-alt"); } saveCloakingConfig(); } function startRotation() { if (rotationInterval) { clearInterval(rotationInterval); } if (cloakingConfig.rotationList.length === 0) { showToast("Add websites to rotation list first", "fa-exclamation-circle"); return; } rotateCloaking(); rotationInterval = setInterval(() => { rotateCloaking(); }, cloakingConfig.rotateSpeed * 1000); } function stopRotation() { if (rotationInterval) { clearInterval(rotationInterval); rotationInterval = null; } } function rotateCloaking() { if (cloakingConfig.rotationList.length === 0) return; const site = cloakingConfig.rotationList[cloakingConfig.currentRotationIndex]; document.title = site.title; setFavicon(site.url); cloakingConfig.currentRotationIndex = (cloakingConfig.currentRotationIndex + 1) % cloakingConfig.rotationList.length; } function renderRotationList() { const container = document.getElementById("rotationList"); if (!container) return; if (cloakingConfig.rotationList.length === 0) { container.innerHTML = '

No websites added yet. Click "Add Website" to get started.

'; return; } container.innerHTML = cloakingConfig.rotationList .map( (site, index) => `
${site.title}
${site.url}
` ) .join(""); } async function addRotationSite() { const title = await prompt("Enter website title (e.g., Google):"); if (!title) return; const url = await prompt("Enter website URL (e.g., https://www.google.com):"); if (!url) return; cloakingConfig.rotationList.push({ title, url }); renderRotationList(); showToast("Website added to rotation", "fa-plus"); } function removeRotationSite(index) { cloakingConfig.rotationList.splice(index, 1); renderRotationList(); showToast("Website removed from rotation", "fa-trash"); } function saveRotationSettings() { const speedInput = document.getElementById("rotateSpeed"); if (speedInput) { const speed = parseInt(speedInput.value); if (speed >= 1 && speed <= 300) { cloakingConfig.rotateSpeed = speed; } } saveCloakingConfig(); if (cloakingConfig.autoRotate) { stopRotation(); startRotation(); } showToast("Rotation settings saved!", "fa-save"); } window.addEventListener("DOMContentLoaded", () => { loadCloakingConfig(); if (cloakingConfig.cloakActive) { applyCloakState(cloakingConfig.cloakTitle, cloakingConfig.cloakFaviconUrl); } if (cloakingConfig.autoRotate) { setTimeout(() => { startRotation(); }, 2000); } if (cloakingConfig.panicKeyEnabled) { setupPanicKeyListener(); } }); const _originalOpenAppForAppStore = openApp; window.openApp = openApp = function (appName, ...args) { _originalOpenAppForAppStore(appName, ...args); if (appName === "appstore") { setTimeout(() => { // Initialize the default themes view with illustrations const activeSection = document.querySelector(".appstore-section.active"); if (activeSection) { switchAppStoreSection("themes", activeSection); } }, 100); } if (appName === "cloaking") { setTimeout(() => { const toggle = document.getElementById("autoRotateToggle"); const settings = document.getElementById("rotateSettings"); if (toggle && cloakingConfig.autoRotate) { toggle.classList.add("active"); if (settings) { settings.style.opacity = "1"; settings.style.pointerEvents = "all"; } } const speedInput = document.getElementById("rotateSpeed"); if (speedInput) { speedInput.value = cloakingConfig.rotateSpeed; updateRotateSpeedDisplay(cloakingConfig.rotateSpeed); } renderRotationList(); const panicUrl = document.getElementById("panicUrl"); if (panicUrl) { panicUrl.value = cloakingConfig.panicUrl || ""; } const panicDisplay = document.getElementById("panicHotkeyDisplay"); if (panicDisplay) { panicDisplay.textContent = cloakingConfig.panicKey || "Click to set hotkey"; } // Initialize Anti-Monitoring Toggles if (cloakingConfig.antiScreenMonitoring) { const amToggle = document.getElementById("antiScreenMonitoringToggle"); const amIndicator = document.querySelectorAll('.cloaking-tab[data-tab="anti-monitor"] .cloaking-status-indicator')[0]; if (amToggle) amToggle.classList.add("active"); if (amIndicator) amIndicator.classList.add("active"); const amSettings = document.getElementById("antiMonitorSettings"); if (amSettings) { amSettings.style.opacity = "1"; amSettings.style.pointerEvents = "auto"; } } if (cloakingConfig.useAntiMonitorDelay) { const delayToggle = document.getElementById("useAntiMonitorDelayToggle"); const delayIndicator = document.querySelectorAll('.cloaking-tab[data-tab="anti-monitor"] .cloaking-status-indicator')[1]; const delayIcon = document.querySelectorAll('.cloaking-tab[data-tab="anti-monitor"] .cloaking-status-icon i')[1]; if (delayToggle) delayToggle.classList.add("active"); if (delayIndicator) delayIndicator.classList.add("active"); if (delayIcon) delayIcon.className = "fas fa-check-circle"; } if (cloakingConfig.confirmPageClosing) { const confirmToggle = document.getElementById("confirmPageClosingToggle"); const confirmIndicator = document.querySelectorAll('.cloaking-tab[data-tab="anti-monitor"] .cloaking-status-indicator')[2]; const confirmIcon = document.querySelectorAll('.cloaking-tab[data-tab="anti-monitor"] .cloaking-status-icon i')[2]; if (confirmToggle) confirmToggle.classList.add("active"); if (confirmIndicator) confirmIndicator.classList.add("active"); if (confirmIcon) confirmIcon.className = "fas fa-check-circle"; } const delayInput = document.getElementById("antiMonitorDelay"); if (delayInput) { delayInput.value = cloakingConfig.antiMonitorDelay; updateAntiMonitorDelayDisplay(cloakingConfig.antiMonitorDelay); } const cloakTitleInput = document.getElementById("cloakTitle"); if (cloakTitleInput) { cloakTitleInput.value = cloakingConfig.cloakActive ? cloakingConfig.cloakTitle : DEFAULT_CLOAK_TITLE; } const cloakFaviconInput = document.getElementById("cloakFavicon"); if (cloakFaviconInput) { cloakFaviconInput.value = cloakingConfig.cloakActive ? cloakingConfig.cloakFaviconUrl : DEFAULT_CLOAK_FAVICON_URL; } updateCloakPreview(); }, 100); } }; function showProperties(appName, x, y) { const tooltip = document.getElementById("propertiesTooltip"); const metadata = appMetadata[appName]; if (!metadata) { showToast("App information not available", "fa-info-circle"); return; } const iconEl = document.getElementById("propIcon"); iconEl.innerHTML = ``; const nameEl = document.getElementById("propName"); nameEl.textContent = metadata.name; const statusEl = document.getElementById("propStatus"); if (metadata.preinstalled) { statusEl.textContent = "Preinstalled"; statusEl.className = "properties-badge preinstalled"; } else { statusEl.textContent = "Installed from App Store"; statusEl.className = "properties-badge installed"; } const typeEl = document.getElementById("propType"); typeEl.textContent = metadata.preinstalled ? "System Application" : "Third-party Application"; tooltip.style.left = x + 15 + "px"; tooltip.style.top = y + "px"; tooltip.classList.add("active"); setTimeout(() => { const rect = tooltip.getBoundingClientRect(); if (rect.right > window.innerWidth) { tooltip.style.left = x - rect.width - 15 + "px"; } if (rect.bottom > window.innerHeight) { tooltip.style.top = y - rect.height + "px"; } if (rect.top < 0) { tooltip.style.top = "10px"; } if (rect.left < 0) { tooltip.style.left = "10px"; } }, 0); } function toggleAntiScreenMonitoring() { cloakingConfig.antiScreenMonitoring = !cloakingConfig.antiScreenMonitoring; saveCloakingConfig(); const toggle = document.getElementById("antiScreenMonitoringToggle"); const indicator = document.querySelector( '.cloaking-tab[data-tab="anti-monitor"] .cloaking-status-indicator' ); const statusDesc = document.querySelector( '.cloaking-tab[data-tab="anti-monitor"] .cloaking-status-desc' ); const antiMonitorSettings = document.getElementById("antiMonitorSettings"); if (cloakingConfig.antiScreenMonitoring) { if (toggle) toggle.classList.add("active"); if (indicator) indicator.classList.add("active"); if (statusDesc) statusDesc.textContent = "Enabled - Nebulo will black out when you switch tabs"; if (antiMonitorSettings) { antiMonitorSettings.style.opacity = "1"; antiMonitorSettings.style.pointerEvents = "auto"; } showToast("Anti-screen monitoring enabled!", "fa-shield-alt"); setupScreenMonitoringListener(); } else { if (toggle) toggle.classList.remove("active"); if (indicator) indicator.classList.remove("active"); if (statusDesc) statusDesc.textContent = "Disabled"; if (antiMonitorSettings) { antiMonitorSettings.style.opacity = "0.5"; antiMonitorSettings.style.pointerEvents = "none"; } showToast("Anti-screen monitoring disabled", "fa-shield"); removeScreenMonitoringListener(); } } function updateAntiMonitorDelayDisplay(value) { cloakingConfig.antiMonitorDelay = parseInt(value); saveCloakingConfig(); const display = document.getElementById("antiMonitorDelayValue"); if (display) { display.textContent = value + "ms"; } } function toggleAntiMonitorDelay() { cloakingConfig.useAntiMonitorDelay = !cloakingConfig.useAntiMonitorDelay; saveCloakingConfig(); const toggle = document.getElementById("useAntiMonitorDelayToggle"); const indicator = document.querySelectorAll( '.cloaking-tab[data-tab="anti-monitor"] .cloaking-status-indicator' )[1]; const statusDesc = document.querySelectorAll( '.cloaking-tab[data-tab="anti-monitor"] .cloaking-status-desc' )[1]; const icon = document.querySelectorAll( '.cloaking-tab[data-tab="anti-monitor"] .cloaking-status-icon i' )[1]; const antiMonitorSettings = document.getElementById("antiMonitorSettings"); if (cloakingConfig.useAntiMonitorDelay) { if (toggle) toggle.classList.add("active"); if (indicator) indicator.classList.add("active"); if (statusDesc) statusDesc.textContent = "Enabled - After switching back to Nebulo your screen will stay black briefly for the configured delay"; if (icon) { icon.className = "fas fa-check-circle"; } if (antiMonitorSettings) { antiMonitorSettings.style.opacity = "1"; antiMonitorSettings.style.pointerEvents = "auto"; } showToast("Unblack delay enabled!", "fa-sliders-h"); } else { if (toggle) toggle.classList.remove("active"); if (indicator) indicator.classList.remove("active"); if (statusDesc) statusDesc.textContent = "Disabled"; if (icon) { icon.className = "fas fa-times-circle"; } if (antiMonitorSettings) { antiMonitorSettings.style.opacity = "0.5"; antiMonitorSettings.style.pointerEvents = "none"; } showToast("Unblack delay disabled!", "fa-times-circle"); } } function toggleConfirmPageClosing() { cloakingConfig.confirmPageClosing = !cloakingConfig.confirmPageClosing; saveCloakingConfig(); const toggle = document.getElementById("confirmPageClosingToggle"); const indicator = document.querySelectorAll( '.cloaking-tab[data-tab="anti-monitor"] .cloaking-status-indicator' )[2]; const statusDesc = document.querySelectorAll( '.cloaking-tab[data-tab="anti-monitor"] .cloaking-status-desc' )[2]; const icon = document.querySelectorAll( '.cloaking-tab[data-tab="anti-monitor"] .cloaking-status-icon i' )[2]; if (cloakingConfig.confirmPageClosing) { if (toggle) toggle.classList.add("active"); if (indicator) indicator.classList.add("active"); if (statusDesc) statusDesc.textContent = "Enabled - Alert will show before closing"; if (icon) { icon.className = "fas fa-check-circle"; } showToast("Page closing confirmation enabled!", "fa-check-circle"); setupPageClosingListener(); } else { if (toggle) toggle.classList.remove("active"); if (indicator) indicator.classList.remove("active"); if (statusDesc) statusDesc.textContent = "Disabled"; if (icon) { icon.className = "fas fa-times-circle"; } showToast("Page closing confirmation disabled", "fa-times-circle"); removePageClosingListener(); } } let screenMonitoringListener = null; let pageClosingListener = null; let blackoutTimeout = null; function setupScreenMonitoringListener() { if (screenMonitoringListener) return; screenMonitoringListener = () => { if (!cloakingConfig.antiScreenMonitoring) return; // Determine current focused window (if any) const currentFocused = typeof focusedWindow !== 'undefined' ? focusedWindow : null; // If we've already shown a blackout for this focused app, don't recreate it repeatedly if (screenMonitoringListener.lastShownForApp && screenMonitoringListener.lastShownForApp === currentFocused) { return; } // If focus is back on the cloaking window or nothing to protect, clear lastShownForApp and bail if (!document.hidden && (!currentFocused || currentFocused === 'cloaking' || !windows["cloaking"])) { screenMonitoringListener.lastShownForApp = null; return; } // If an overlay already exists, a previous trigger is still active — keep it const existingOverlay = document.getElementById("screenMonitoringBlackout"); if (existingOverlay) { // Do not recreate or remove it here; let the original timeout handle cleanup return; } // Create a fullscreen blackout overlay using the same method as the integrity check const blackoutOverlay = document.createElement("div"); blackoutOverlay.id = "screenMonitoringBlackout"; blackoutOverlay.style.cssText = "position:fixed;inset:0;background:var(--pure-black);z-index:99999;display:block;"; document.body.appendChild(blackoutOverlay); // Record which app we showed the blackout for so we don't spam it repeatedly screenMonitoringListener.lastShownForApp = currentFocused || 'hidden'; // If delay is disabled, remove immediately, otherwise use the delay if (!cloakingConfig.useAntiMonitorDelay) { blackoutTimeout = setTimeout(() => { const overlay = document.getElementById("screenMonitoringBlackout"); if (overlay) { overlay.remove(); } }, 0); } else { blackoutTimeout = setTimeout(() => { const overlay = document.getElementById("screenMonitoringBlackout"); if (overlay) { overlay.remove(); } }, cloakingConfig.antiMonitorDelay); } }; window.addEventListener("visibilitychange", screenMonitoringListener); window.addEventListener("blur", screenMonitoringListener); // Also monitor for when another window gets focus (higher z-index) let focusCheckInterval = setInterval(() => { if (!cloakingConfig.antiScreenMonitoring) { clearInterval(focusCheckInterval); return; } // If cloaking window is not the focused window, show the blackout if (focusedWindow && focusedWindow !== "cloaking" && windows["cloaking"]) { screenMonitoringListener(); } else { // If cloaking regained focus, clear the last shown marker so future switches will trigger again if (focusedWindow === "cloaking" || !windows["cloaking"]) { if (screenMonitoringListener && screenMonitoringListener.lastShownForApp) screenMonitoringListener.lastShownForApp = null; } } }, 300); // Store the interval ID so we can clear it later screenMonitoringListener.focusCheckInterval = focusCheckInterval; } function removeScreenMonitoringListener() { if (screenMonitoringListener) { window.removeEventListener("visibilitychange", screenMonitoringListener); window.removeEventListener("blur", screenMonitoringListener); // Clear the focus check interval if it exists if (screenMonitoringListener.focusCheckInterval) { clearInterval(screenMonitoringListener.focusCheckInterval); } screenMonitoringListener = null; } if (blackoutTimeout) { clearTimeout(blackoutTimeout); blackoutTimeout = null; } } function setupPageClosingListener() { if (pageClosingListener) return; pageClosingListener = (e) => { if (!cloakingConfig.confirmPageClosing) return; // Modern browsers block confirmation dialogs for security reasons // Instead, we'll just log that the user is leaving console.log("User is leaving the page"); }; window.addEventListener("beforeunload", pageClosingListener); } function removePageClosingListener() { if (pageClosingListener) { window.removeEventListener("beforeunload", pageClosingListener); pageClosingListener = null; } } if (cloakingConfig.antiScreenMonitoring) setupScreenMonitoringListener(); if (cloakingConfig.confirmPageClosing) setupPageClosingListener(); function hideProperties() { const tooltip = document.getElementById("propertiesTooltip"); tooltip.classList.remove("active"); } document.addEventListener("click", (e) => { const tooltip = document.getElementById("propertiesTooltip"); if ( tooltip && !tooltip.contains(e.target) && !e.target.closest(".context-menu") ) { hideProperties(); } }); document.addEventListener("contextmenu", () => { hideProperties(); }); function updateLoginScreen() { // Check for multi-user accounts const accounts = getAllAccounts(); if (accounts.length > 0) { // Multi-user system - show account list updateLoginWithAccounts(accounts); } else { // Old single-user system const isPasswordless = localStorage.getItem("nebulo_isPasswordless") === "true"; const passwordWrapper = document.getElementById("passwordWrapper"); const loginSubtitle = document.getElementById("loginSubtitle"); const loginContainer = document.querySelector(".login-container"); if (isPasswordless) { if (passwordWrapper) passwordWrapper.style.display = "none"; if (loginSubtitle) loginSubtitle.textContent = "Passwordless account - just enter your username"; if (loginContainer) loginContainer.classList.add("passwordless"); } else { if (passwordWrapper) passwordWrapper.style.display = "block"; if (loginSubtitle) loginSubtitle.textContent = "Sign in to continue"; if (loginContainer) loginContainer.classList.remove("passwordless"); } } } function updateLoginWithAccounts(accounts) { const loginSubtitle = document.getElementById("loginSubtitle"); const usernameInput = document.getElementById("username"); const passwordWrapper = document.getElementById("passwordWrapper"); if (accounts.length === 1) { // Only one account, pre-fill username const account = accounts[0]; if (usernameInput) { usernameInput.value = account.username; } if (loginSubtitle) { loginSubtitle.textContent = account.isPasswordless ? "Passwordless account - click Sign In" : "Enter your password"; } if (passwordWrapper) { passwordWrapper.style.display = account.isPasswordless ? "none" : "block"; } } else { // Multiple accounts - show account selector if (loginSubtitle) { loginSubtitle.innerHTML = `
Select an account:
${accounts.map(acc => ` `).join('')}
`; } // Hide username and password fields initially if (usernameInput) usernameInput.style.display = "none"; if (passwordWrapper) passwordWrapper.style.display = "none"; } } function selectLoginAccount(username) { const account = getAccountByUsername(username); if (!account) return; const usernameInput = document.getElementById("username"); const passwordInput = document.getElementById("password"); const passwordWrapper = document.getElementById("passwordWrapper"); const loginSubtitle = document.getElementById("loginSubtitle"); // Fill username if (usernameInput) { usernameInput.value = username; usernameInput.style.display = "block"; } // Show/hide password field if (passwordWrapper) { passwordWrapper.style.display = account.isPasswordless ? "none" : "block"; } // Update subtitle if (loginSubtitle) { loginSubtitle.innerHTML = account.isPasswordless ? ` Back to account list` : `Enter password for ${username} ← Back`; } // Focus password input if needed if (!account.isPasswordless && passwordInput) { setTimeout(() => passwordInput.focus(), 100); } } function switchSettingsTab(tabName, element) { document.querySelectorAll(".settings-nav-item").forEach((item) => { item.classList.remove("active"); }); element.classList.add("active"); document.querySelectorAll(".settings-tab-content").forEach((content) => { content.classList.remove("active"); }); const targetContent = document.querySelector( `.settings-tab-content[data-tab="${tabName}"]` ); if (targetContent) { targetContent.classList.add("active"); } } function expandHelpTopic(topicId) { const expandedView = document.getElementById("helpExpandedView"); const expandedContent = document.getElementById("helpExpandedContent"); const topics = { welcome: { title: "Welcome to Nebulo", icon: "fa-info-circle", content: `

Welcome to Nebulo

Nebulo is a fully-featured web-based operating system with a complete desktop environment, virtual file system, and productivity applications.

This help system will guide you through all the features and capabilities of Nebulo. Select any topic from the main menu to learn more.

`, }, cloaking: { title: "Cloaking", icon: "fa-mask", content: `

Cloaking

Disguise your browser tab with custom titles and favicons, as well as auto-rotate features to keep your tab constantly changing.

Features

`, }, boot: { title: "Boot Options", icon: "fa-power-off", content: `

Boot Options

Nebulo offers two boot modes:

Your boot choice is remembered automatically. To change it, open Settings → System → Reset Boot Preference.

`, }, apps: { title: "Applications", icon: "fa-th", content: `

Applications

Pre-installed Apps

Optional Apps (Available in App Store)

`, }, desktop: { title: "Desktop Features", icon: "fa-desktop", content: `

Desktop Features

Desktop Icons

Start Menu

Click the fish icon in the taskbar to access all applications, view your profile, and sign out or shut down the system.

Taskbar

Windows

`, }, notifications: { title: "Notifications & Quick Actions", icon: "fa-bell", content: `

Notifications & Quick Actions

Quick Actions Panel

Click the bolt icon in the taskbar to access:

Notification Center

Click the bell icon to view your notification history. All system messages are stored here so you can review them later.

`, }, screenshots: { title: "Taking Screenshots", icon: "fa-camera", content: `

Taking Screenshots

Capture your desktop in just a few steps:

How to Take a Screenshot

Screenshot Features

`, }, settings: { title: "Settings & Customization", icon: "fa-cog", content: `

Settings & Customization

General Settings

Appearance

System

Account

Advanced

`, }, tips: { title: "Tips & Tricks", icon: "fa-lightbulb", content: `

Tips & Tricks

Productivity Tips

Keyboard Shortcuts

Hidden Features

Best Practices

`, }, 'app-files': { title: "Files", icon: "fa-folder", content: `

Files

The Files app is your file manager for browsing and organizing your virtual file system.

Features

Supported File Types

`, }, 'app-terminal': { title: "Terminal", icon: "fa-terminal", content: `

Terminal

The Terminal provides a command-line interface for advanced operations.

Available Commands

Tips

`, }, 'app-editor': { title: "Text Editor", icon: "fa-edit", content: `

Text Editor

Create and edit text files with this simple but powerful editor.

Features

Keyboard Shortcuts

`, }, 'app-about': { title: "About Nebulo", icon: "fa-info-circle", content: `

About Nebulo

Learn more about Nebulo, its features, and development.

Information

`, }, 'app-settings': { title: "Settings", icon: "fa-cog", content: `

Settings

Customize your Nebulo experience with various options.

Available Settings

`, }, 'app-appstore': { title: "App Store", icon: "fa-store", content: `

App Store

Browse and install additional apps and themes for Nebulo.

Sections

Installation

`, }, 'app-calculator': { title: "Calculator", icon: "fa-calculator", content: `

Calculator

Perform basic mathematical calculations.

Operations

`, }, 'app-browser': { title: "Browser", icon: "fa-globe", content: `

Browser

Browse the web with the built-in Scramjet-powered browser.

Features

Tips

`, }, 'app-music': { title: "Music", icon: "fa-music", content: `

Music

Play and manage your audio files.

Features

Supported Formats

`, }, 'app-photos': { title: "Photos", icon: "fa-images", content: `

Photos

View and manage your images and screenshots.

Features

Supported Formats

`, }, 'app-python': { title: "Python Interpreter", icon: "fa-code", content: `

Python Interpreter

Run Python code interactively or execute Python files.

Features

Example Code

print('Hello from Nebulo!')
x = 10
y = 20
result = x + y
print(f'Result: {result}')
`, }, 'app-achievements': { title: "Achievements", icon: "fa-trophy", content: `

Achievements

Track your progress and unlock achievements as you explore Nebulo.

Features

Tips

`, }, 'app-whatsnew': { title: "What's New", icon: "fa-star", content: `

What's New

Stay updated with the latest features and improvements in Nebulo.

Features

Current Version

Nebulo v1.5 includes AI Assistant, multiple browsers, more themes, VS Code integration, window snapping, games, and more!

`, }, }; const topic = topics[topicId]; if (!topic) return; expandedContent.innerHTML = topic.content; expandedView.classList.add("active"); } function closeHelpTopic() { const expandedView = document.getElementById("helpExpandedView"); expandedView.classList.remove("active"); } function switchCloakingTab(tabName, element) { document.querySelectorAll(".cloaking-nav-item").forEach((item) => { item.classList.remove("active"); }); element.classList.add("active"); document.querySelectorAll(".cloaking-tab").forEach((content) => { content.classList.remove("active"); }); const targetContent = document.querySelector( `.cloaking-tab[data-tab="${tabName}"]` ); if (targetContent) { targetContent.classList.add("active"); } if (tabName === "rotate") { renderRotationList(); } } function updateCloakPreview() { const titleInput = document.getElementById("cloakTitle"); const faviconInput = document.getElementById("cloakFavicon"); const previewTitle = document.getElementById("previewTitle"); const previewFavicon = document.getElementById("previewFavicon"); if (!titleInput || !previewTitle) return; const title = titleInput.value.trim() || "Nebulo"; previewTitle.textContent = title; if (faviconInput && previewFavicon) { const url = faviconInput.value.trim(); if (url) { let domain = url; try { const urlObj = new URL(url.startsWith("http") ? url : "https://" + url); domain = urlObj.origin; } catch (e) { domain = "https://" + url.replace(/^https?:\/\//, ""); } previewFavicon.src = `https://www.google.com/s2/favicons?domain=${domain}&sz=64`; } else { previewFavicon.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='75' font-size='75' fill='white'%3E🌐︎%3C/text%3E%3C/svg%3E"; } } } function updateRotateSpeedDisplay(value) { const display = document.getElementById("rotateSpeedValue"); if (display) { display.textContent = value + "s"; } } function togglePanicKey() { cloakingConfig.panicKeyEnabled = !cloakingConfig.panicKeyEnabled; saveCloakingConfig(); const toggle = document.querySelector( '.cloaking-tab[data-tab="panic"] .toggle-switch' ); const indicator = document.querySelector( '.cloaking-tab[data-tab="panic"] .cloaking-status-indicator' ); const statusDesc = document.querySelector( '.cloaking-tab[data-tab="panic"] .cloaking-status-desc' ); if (cloakingConfig.panicKeyEnabled) { if (toggle) toggle.classList.add("active"); if (indicator) indicator.classList.add("active"); if (statusDesc) statusDesc.textContent = "Armed and Ready"; showToast( "Panic key enabled! Press " + cloakingConfig.panicKey + " to activate", "fa-shield-alt" ); setupPanicKeyListener(); } else { if (toggle) toggle.classList.remove("active"); if (indicator) indicator.classList.remove("active"); if (statusDesc) statusDesc.textContent = "Disabled"; showToast("Panic key disabled", "fa-shield"); removePanicKeyListener(); } } let isRecordingPanicKey = false; let panicKeyListener = null; function recordPanicKey() { if (isRecordingPanicKey) return; const display = document.getElementById("panicHotkeyDisplay"); if (!display) return; isRecordingPanicKey = true; display.textContent = "Press any key..."; display.classList.add("recording"); const recordListener = (e) => { e.preventDefault(); let keyCombo = []; if (e.ctrlKey) keyCombo.push("Ctrl"); if (e.altKey) keyCombo.push("Alt"); if (e.shiftKey) keyCombo.push("Shift"); if (e.metaKey) keyCombo.push("Meta"); if (!["Control", "Alt", "Shift", "Meta"].includes(e.key)) { keyCombo.push(e.key); } const keyString = keyCombo.join(" + "); cloakingConfig.panicKey = keyString; display.textContent = keyString; display.classList.remove("recording"); isRecordingPanicKey = false; saveCloakingConfig(); showToast("Panic key set to: " + keyString, "fa-keyboard"); document.removeEventListener("keydown", recordListener); if (cloakingConfig.panicKeyEnabled) { removePanicKeyListener(); setupPanicKeyListener(); } }; document.addEventListener("keydown", recordListener); } function setupPanicKeyListener() { removePanicKeyListener(); panicKeyListener = (e) => { if (!cloakingConfig.panicKeyEnabled) return; let pressedCombo = []; if (e.ctrlKey) pressedCombo.push("Ctrl"); if (e.altKey) pressedCombo.push("Alt"); if (e.shiftKey) pressedCombo.push("Shift"); if (e.metaKey) pressedCombo.push("Meta"); if (!["Control", "Alt", "Shift", "Meta"].includes(e.key)) { pressedCombo.push(e.key); } const pressedString = pressedCombo.join(" + "); if (pressedString === cloakingConfig.panicKey) { e.preventDefault(); triggerPanicMode(); } }; document.addEventListener("keydown", panicKeyListener); } function removePanicKeyListener() { if (panicKeyListener) { document.removeEventListener("keydown", panicKeyListener); panicKeyListener = null; } } function testPanicKey() { const url = document.getElementById("panicUrl")?.value.trim(); if (!url) { showToast("Please enter a panic URL first", "fa-exclamation-circle"); return; } showToast("Testing panic redirect in 2 seconds...", "fa-vial"); setTimeout(() => { triggerPanicMode(); }, 2000); } function triggerPanicMode() { const url = cloakingConfig.panicUrl || "https://classroom.google.com"; Object.keys(windows).forEach((appName) => { const win = windows[appName]; if (win) { win.style.display = "none"; } }); let domain = url; try { const urlObj = new URL(url.startsWith("http") ? url : "https://" + url); domain = urlObj.origin; document.title = urlObj.hostname; } catch (e) { domain = "https://" + url.replace(/^https?:\/\//, ""); document.title = url.replace(/^https?:\/\//, "").split("/")[0]; } setFavicon(domain); window.location.href = url.startsWith("http") ? url : "https://" + url; } function changeSearchEngine(value) { console.log('[DEBUG] changeSearchEngine called with value:', value); localStorage.setItem('nOS_searchEngine', value); showToast('Search engine updated!', 'fa-check-circle'); } function applyPreset(presetName) { const presets = { google: { title: "Google", url: "https://www.google.com", }, gmail: { title: "Gmail", url: "https://mail.google.com", }, drive: { title: "Google Drive", url: "https://drive.google.com", }, classroom: { title: "Google Classroom", url: "https://classroom.google.com", }, docs: { title: "Google Docs", url: "https://docs.google.com", }, youtube: { title: "YouTube", url: "https://www.youtube.com", }, wikipedia: { title: "Wikipedia", url: "https://www.wikipedia.org", }, github: { title: "GitHub", url: "https://github.com", }, }; const preset = presets[presetName]; if (!preset) return; document.title = preset.title; setFavicon(preset.url); showToast(`Applied ${preset.title} preset!`, "fa-check-circle"); unlockAchievement("stealth-mode"); } function checkImportedAchievements() { if (installedThemes.length > 0) { unlockAchievement("theme-changer"); } const preinstalledApps = [ "files", "terminal", "browser", "games", "blooket-bot", "minecraft", "settings", "editor", "music", "photos", "help", "whatsnew", "appstore", "calculator", "cloaking", ].filter((app) => !hiddenAppIds.has(app)); installedApps.forEach((appName) => { if (!preinstalledApps.includes(appName)) { achievementsData.openedApps.add(appName); } }); const allAppsAchievement = achievementsData.achievements["all-apps"]; if (allAppsAchievement) { allAppsAchievement.progress = achievementsData.openedApps.size; if (achievementsData.openedApps.size >= allAppsAchievement.target) { unlockAchievement("all-apps"); } } saveAchievements(); } function openChangeUsernameDialog() { showModal( "Change Username", "fa-user-edit", "Enter your new username:", "changeUsername", "Change", "Cancel" ); const inputContainer = document.getElementById("modalInputContainer"); inputContainer.innerHTML = ` `; setTimeout(() => { const input = document.getElementById("newUsernameInput"); if (input) { input.focus(); input.select(); } }, 100); } function changeUsername() { const newUsernameInput = document.getElementById("newUsernameInput"); if (!newUsernameInput) return; const newUsername = newUsernameInput.value.trim(); if (!newUsername) { showToast("Username cannot be empty", "fa-exclamation-circle"); return; } if (newUsername.length < 3) { showToast("Username must be at least 3 characters long", "fa-exclamation-circle"); return; } if (newUsername.length > 20) { showToast("Username must be 20 characters or less", "fa-exclamation-circle"); return; } if (!/^[a-zA-Z0-9_-]+$/.test(newUsername)) { showToast("Username can only contain letters, numbers, underscores, and hyphens", "fa-exclamation-circle"); return; } if (newUsername === currentUsername) { showToast("New username is the same as current username", "fa-info-circle"); closeModal(); return; } currentUsername = newUsername; localStorage.setItem("nebulo_username", newUsername); updateUsernameBadges(newUsername); showToast(`Username changed to "${newUsername}"`, "fa-check-circle"); closeModal(); if (windows["settings"]) { closeWindow( windows["settings"].querySelector(".window-btn.close"), "settings" ); } } let game2048 = { board: null, score: 0, best: localStorage.getItem('2048Best') ? parseInt(localStorage.getItem('2048Best')) : 0, size: 4, gameStarted: false, tiles: [], mergedTiles: new Set(), newTiles: new Set(), previousBoard: null, slideDirection: null }; function start2048Game() { game2048.board = Array(game2048.size).fill().map(() => Array(game2048.size).fill(0)); game2048.score = 0; game2048.gameStarted = true; game2048.tiles = []; game2048.mergedTiles = new Set(); game2048.newTiles = new Set(); game2048.previousBoard = null; game2048.slideDirection = null; document.getElementById('game2048Score').textContent = '0'; document.getElementById('game2048Best').textContent = game2048.best; addRandomTile2048(); addRandomTile2048(); render2048Board(); document.removeEventListener('keydown', handle2048KeyPress); document.addEventListener('keydown', handle2048KeyPress); } function addRandomTile2048() { const emptyCells = []; for (let i = 0; i < game2048.size; i++) { for (let j = 0; j < game2048.size; j++) { if (game2048.board[i][j] === 0) { emptyCells.push({ row: i, col: j }); } } } if (emptyCells.length > 0) { const cell = emptyCells[Math.floor(Math.random() * emptyCells.length)]; game2048.board[cell.row][cell.col] = Math.random() < 0.9 ? 2 : 4; game2048.newTiles.add(`${cell.row}-${cell.col}`); } } function getTileColor(value) { const accentColor = getComputedStyle(document.documentElement).getPropertyValue('--accent').trim(); const bgSecondary = getComputedStyle(document.documentElement).getPropertyValue('--bg-secondary').trim(); if (value === 0) return bgSecondary; if (value === 2) return 'rgba(125, 211, 192, 0.2)'; if (value === 4) return 'rgba(125, 211, 192, 0.3)'; if (value === 8) return 'rgba(125, 211, 192, 0.4)'; if (value === 16) return 'rgba(125, 211, 192, 0.5)'; if (value === 32) return 'rgba(125, 211, 192, 0.6)'; if (value === 64) return 'rgba(125, 211, 192, 0.7)'; if (value === 128) return 'rgba(125, 211, 192, 0.8)'; if (value >= 256) return accentColor; return accentColor; } function getTileTextColor(value) { const textPrimary = getComputedStyle(document.documentElement).getPropertyValue('--text-primary').trim(); const bgPrimary = getComputedStyle(document.documentElement).getPropertyValue('--bg-primary').trim(); if (value >= 8) return textPrimary; return textPrimary; } function render2048Board() { const boardEl = document.getElementById('game2048Board'); if (!boardEl) return; // Create container with absolute positioning for smooth animations boardEl.innerHTML = ''; boardEl.style.display = 'block'; boardEl.style.position = 'relative'; boardEl.style.width = `${game2048.size * 100 + (game2048.size - 1) * 10}px`; boardEl.style.height = `${game2048.size * 100 + (game2048.size - 1) * 10}px`; // Render background grid for (let i = 0; i < game2048.size; i++) { for (let j = 0; j < game2048.size; j++) { const cell = document.createElement('div'); cell.className = 'tile-cell-bg'; cell.style.position = 'absolute'; cell.style.left = `${j * 110}px`; cell.style.top = `${i * 110}px`; cell.style.width = '100px'; cell.style.height = '100px'; cell.style.background = getComputedStyle(document.documentElement).getPropertyValue('--bg-secondary').trim(); cell.style.border = '2px solid var(--border)'; cell.style.borderRadius = '8px'; cell.style.opacity = '0.3'; boardEl.appendChild(cell); } } // Render actual tiles with positions const tileContainer = document.createElement('div'); tileContainer.style.position = 'absolute'; tileContainer.style.top = '0'; tileContainer.style.left = '0'; tileContainer.style.width = '100%'; tileContainer.style.height = '100%'; boardEl.appendChild(tileContainer); for (let i = 0; i < game2048.size; i++) { for (let j = 0; j < game2048.size; j++) { const value = game2048.board[i][j]; if (value === 0) continue; const tile = document.createElement('div'); tile.className = 'tile-2048'; tile.dataset.row = i; tile.dataset.col = j; tile.dataset.value = value; // Calculate position const left = j * 110; const top = i * 110; tile.style.position = 'absolute'; tile.style.left = `${left}px`; tile.style.top = `${top}px`; tile.style.width = '100px'; tile.style.height = '100px'; tile.style.background = getTileColor(value); tile.style.border = '2px solid var(--border)'; tile.style.borderRadius = '8px'; tile.style.display = 'flex'; tile.style.alignItems = 'center'; tile.style.justifyContent = 'center'; tile.style.fontSize = value >= 1000 ? '28px' : value >= 100 ? '36px' : '44px'; tile.style.fontWeight = 'bold'; tile.style.fontFamily = 'fontb'; tile.style.color = getTileTextColor(value); tile.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)'; tile.style.zIndex = '10'; // Animation handling if (game2048.newTiles.has(`${i}-${j}`)) { tile.style.transform = 'scale(0)'; tile.style.transition = 'transform 0.2s ease'; setTimeout(() => { tile.style.transform = 'scale(1)'; }, 50); } else if (game2048.mergedTiles.has(`${i}-${j}`)) { tile.style.transition = 'all 0.15s ease, transform 0.2s ease'; setTimeout(() => { tile.style.transform = 'scale(1.1)'; setTimeout(() => { tile.style.transform = 'scale(1)'; }, 100); }, 150); } else if (game2048.previousBoard && game2048.slideDirection) { // Find previous position and animate slide const prevPos = findPreviousPosition(i, j, value); if (prevPos) { const prevLeft = prevPos.col * 110; const prevTop = prevPos.row * 110; tile.style.left = `${prevLeft}px`; tile.style.top = `${prevTop}px`; tile.style.transition = 'all 0.15s ease'; setTimeout(() => { tile.style.left = `${left}px`; tile.style.top = `${top}px`; }, 10); } else { tile.style.transition = 'all 0.15s ease'; } } tile.textContent = value; tileContainer.appendChild(tile); } } // Clear animation tracking setTimeout(() => { game2048.newTiles.clear(); game2048.mergedTiles.clear(); game2048.previousBoard = null; game2048.slideDirection = null; }, 200); } // New helper function to find previous position function findPreviousPosition(currentRow, currentCol, value) { if (!game2048.previousBoard) return null; for (let i = 0; i < game2048.size; i++) { for (let j = 0; j < game2048.size; j++) { if (game2048.previousBoard[i][j] === value) { // Check if this could be the source tile const rowDiff = Math.abs(i - currentRow); const colDiff = Math.abs(j - currentCol); if (game2048.slideDirection === 'up' && j === currentCol && i > currentRow) { return { row: i, col: j }; } else if (game2048.slideDirection === 'down' && j === currentCol && i < currentRow) { return { row: i, col: j }; } else if (game2048.slideDirection === 'left' && i === currentRow && j > currentCol) { return { row: i, col: j }; } else if (game2048.slideDirection === 'right' && i === currentRow && j < currentCol) { return { row: i, col: j }; } } } } return null; } function getSlideAnimationClass(row, col, direction) { if (!game2048.previousBoard) return null; // Find where this tile was in the previous board for (let i = 0; i < game2048.size; i++) { for (let j = 0; j < game2048.size; j++) { if (game2048.previousBoard[i][j] === game2048.board[row][col] && game2048.previousBoard[i][j] !== 0) { // Check if it's the same tile that moved if (i !== row || j !== col) { switch (direction) { case 'left': return j < col ? 'tile-slide-left' : null; case 'right': return j > col ? 'tile-slide-right' : null; case 'up': return i < row ? 'tile-slide-up' : null; case 'down': return i > row ? 'tile-slide-down' : null; } } break; } } } return null; } function handle2048KeyPress(e) { if (!game2048.gameStarted) return; const key = e.key.toLowerCase(); let moved = false; let direction = ''; let scoreBefore = game2048.score; // Store previous board state for slide animations game2048.previousBoard = game2048.board.map(row => [...row]); if (key === 'arrowup' || key === 'w') { e.preventDefault(); moved = move2048Up(); direction = 'up'; } else if (key === 'arrowdown' || key === 's') { e.preventDefault(); moved = move2048Down(); direction = 'down'; } else if (key === 'arrowleft' || key === 'a') { e.preventDefault(); moved = move2048Left(); direction = 'left'; } else if (key === 'arrowright' || key === 'd') { e.preventDefault(); moved = move2048Right(); direction = 'right'; } if (moved) { game2048.slideDirection = direction; addRandomTile2048(); setTimeout(() => { render2048Board(); }, 100); // Animate score if it increased if (game2048.score > scoreBefore) { const scoreEl = document.getElementById('game2048Score'); scoreEl.classList.add('score-increase'); setTimeout(() => { scoreEl.classList.remove('score-increase'); }, 300); } if (game2048.score > game2048.best) { game2048.best = game2048.score; localStorage.setItem('2048Best', game2048.best); document.getElementById('game2048Best').textContent = game2048.best; } if (check2048GameOver()) { game2048.gameStarted = false; setTimeout(() => { showToast('Game Over! Score: ' + game2048.score, 'fa-gamepad'); }, 300); } } else { // Reset if no move was made game2048.previousBoard = null; game2048.slideDirection = null; } } function move2048Left() { let moved = false; for (let i = 0; i < game2048.size; i++) { const row = game2048.board[i].filter(val => val !== 0); for (let j = 0; j < row.length - 1; j++) { if (row[j] === row[j + 1]) { row[j] *= 2; game2048.score += row[j]; document.getElementById('game2048Score').textContent = game2048.score; row.splice(j + 1, 1); // Track merged tile position game2048.mergedTiles.add(`${i}-${j}`); } } while (row.length < game2048.size) row.push(0); if (JSON.stringify(game2048.board[i]) !== JSON.stringify(row)) moved = true; game2048.board[i] = row; } return moved; } function move2048Right() { let moved = false; for (let i = 0; i < game2048.size; i++) { const row = game2048.board[i].filter(val => val !== 0); for (let j = row.length - 1; j > 0; j--) { if (row[j] === row[j - 1]) { row[j] *= 2; game2048.score += row[j]; document.getElementById('game2048Score').textContent = game2048.score; row.splice(j - 1, 1); j--; // Track merged tile position game2048.mergedTiles.add(`${i}-${game2048.size - row.length + j}`); } } while (row.length < game2048.size) row.unshift(0); if (JSON.stringify(game2048.board[i]) !== JSON.stringify(row)) moved = true; game2048.board[i] = row; } return moved; } function move2048Up() { let moved = false; for (let j = 0; j < game2048.size; j++) { const col = []; for (let i = 0; i < game2048.size; i++) { if (game2048.board[i][j] !== 0) col.push(game2048.board[i][j]); } for (let i = 0; i < col.length - 1; i++) { if (col[i] === col[i + 1]) { col[i] *= 2; game2048.score += col[i]; document.getElementById('game2048Score').textContent = game2048.score; col.splice(i + 1, 1); // Track merged tile position game2048.mergedTiles.add(`${i}-${j}`); } } while (col.length < game2048.size) col.push(0); for (let i = 0; i < game2048.size; i++) { if (game2048.board[i][j] !== col[i]) moved = true; game2048.board[i][j] = col[i]; } } return moved; } function move2048Down() { let moved = false; for (let j = 0; j < game2048.size; j++) { const col = []; for (let i = 0; i < game2048.size; i++) { if (game2048.board[i][j] !== 0) col.push(game2048.board[i][j]); } for (let i = col.length - 1; i > 0; i--) { if (col[i] === col[i - 1]) { col[i] *= 2; game2048.score += col[i]; document.getElementById('game2048Score').textContent = game2048.score; col.splice(i - 1, 1); i--; // Track merged tile position game2048.mergedTiles.add(`${game2048.size - col.length + i}-${j}`); } } while (col.length < game2048.size) col.unshift(0); for (let i = 0; i < game2048.size; i++) { if (game2048.board[i][j] !== col[i]) moved = true; game2048.board[i][j] = col[i]; } } return moved; } function check2048GameOver() { for (let i = 0; i < game2048.size; i++) { for (let j = 0; j < game2048.size; j++) { if (game2048.board[i][j] === 0) return false; if (j < game2048.size - 1 && game2048.board[i][j] === game2048.board[i][j + 1]) return false; if (i < game2048.size - 1 && game2048.board[i][j] === game2048.board[i + 1][j]) return false; } } return true; } let tttGame = { board: Array(9).fill(''), currentPlayer: 'X', gameActive: true, wins: localStorage.getItem('tttWins') ? parseInt(localStorage.getItem('tttWins')) : 0, losses: localStorage.getItem('tttLosses') ? parseInt(localStorage.getItem('tttLosses')) : 0, draws: localStorage.getItem('tttDraws') ? parseInt(localStorage.getItem('tttDraws')) : 0 }; function startTicTacToe() { tttGame.board = Array(9).fill(''); tttGame.currentPlayer = 'X'; tttGame.gameActive = true; document.getElementById('tttStatus').textContent = 'Your turn (X)'; document.getElementById('tttWins').textContent = tttGame.wins; document.getElementById('tttLosses').textContent = tttGame.losses; document.getElementById('tttDraws').textContent = tttGame.draws; renderTTTBoard(); } function renderTTTBoard() { const boardEl = document.getElementById('tttBoard'); if (!boardEl) return; const accentColor = getComputedStyle(document.documentElement).getPropertyValue('--accent').trim(); const textPrimary = getComputedStyle(document.documentElement).getPropertyValue('--text-primary').trim(); const bgSecondary = getComputedStyle(document.documentElement).getPropertyValue('--bg-secondary').trim(); const border = getComputedStyle(document.documentElement).getPropertyValue('--border').trim(); boardEl.innerHTML = ''; for (let i = 0; i < 9; i++) { const cell = document.createElement('div'); cell.style.width = '120px'; cell.style.height = '120px'; cell.style.background = bgSecondary; cell.style.border = `2px solid ${border}`; cell.style.borderRadius = '12px'; cell.style.display = 'flex'; cell.style.alignItems = 'center'; cell.style.justifyContent = 'center'; cell.style.fontSize = '56px'; cell.style.fontWeight = 'bold'; cell.style.fontFamily = 'fontb'; cell.style.cursor = tttGame.board[i] === '' && tttGame.gameActive ? 'pointer' : 'default'; cell.style.transition = 'all 0.2s ease'; cell.style.color = tttGame.board[i] === 'X' ? accentColor : textPrimary; cell.textContent = tttGame.board[i]; if (tttGame.board[i] === '' && tttGame.gameActive) { cell.onmouseover = () => { cell.style.background = `rgba(125, 211, 192, 0.1)`; cell.style.transform = 'scale(1.05)'; cell.style.borderColor = accentColor; }; cell.onmouseout = () => { cell.style.background = bgSecondary; cell.style.transform = 'scale(1)'; cell.style.borderColor = border; }; } cell.onclick = () => handleTTTCellClick(i); boardEl.appendChild(cell); } } function handleTTTCellClick(index) { if (tttGame.board[index] !== '' || !tttGame.gameActive || tttGame.currentPlayer !== 'X') return; tttGame.board[index] = 'X'; renderTTTBoard(); const result = checkTTTWinner(); if (result) { endTTTGame(result); return; } if (tttGame.board.every(cell => cell !== '')) { endTTTGame('draw'); return; } tttGame.currentPlayer = 'O'; document.getElementById('tttStatus').textContent = 'AI is thinking...'; setTimeout(() => { makeAIMove(); }, 500); } function makeAIMove() { const bestMove = getBestMove(); tttGame.board[bestMove] = 'O'; renderTTTBoard(); const result = checkTTTWinner(); if (result) { endTTTGame(result); return; } if (tttGame.board.every(cell => cell !== '')) { endTTTGame('draw'); return; } tttGame.currentPlayer = 'X'; document.getElementById('tttStatus').textContent = 'Your turn (X)'; } function getBestMove() { for (let i = 0; i < 9; i++) { if (tttGame.board[i] === '') { tttGame.board[i] = 'O'; if (checkTTTWinner() === 'O') { tttGame.board[i] = ''; return i; } tttGame.board[i] = ''; } } for (let i = 0; i < 9; i++) { if (tttGame.board[i] === '') { tttGame.board[i] = 'X'; if (checkTTTWinner() === 'X') { tttGame.board[i] = ''; return i; } tttGame.board[i] = ''; } } const corners = [0, 2, 6, 8]; const availableCorners = corners.filter(i => tttGame.board[i] === ''); if (availableCorners.length > 0) { return availableCorners[Math.floor(Math.random() * availableCorners.length)]; } if (tttGame.board[4] === '') return 4; const available = tttGame.board.map((cell, i) => cell === '' ? i : null).filter(i => i !== null); return available[Math.floor(Math.random() * available.length)]; } function checkTTTWinner() { const winPatterns = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ]; for (const pattern of winPatterns) { const [a, b, c] = pattern; if (tttGame.board[a] && tttGame.board[a] === tttGame.board[b] && tttGame.board[a] === tttGame.board[c]) { return tttGame.board[a]; } } return null; } function endTTTGame(result) { tttGame.gameActive = false; const status = document.getElementById('tttStatus'); if (result === 'X') { tttGame.wins++; localStorage.setItem('tttWins', tttGame.wins); status.innerHTML = 'You won! '; document.getElementById('tttWins').textContent = tttGame.wins; showToast('You won!', 'fa-trophy'); } else if (result === 'O') { tttGame.losses++; localStorage.setItem('tttLosses', tttGame.losses); status.innerHTML = 'AI won! '; document.getElementById('tttLosses').textContent = tttGame.losses; showToast('AI won!', 'fa-robot'); } else { tttGame.draws++; localStorage.setItem('tttDraws', tttGame.draws); status.innerHTML = "It's a draw! "; document.getElementById('tttDraws').textContent = tttGame.draws; showToast("It's a draw!", 'fa-handshake'); } } // V86 Emulator Resource Management async function loadV86ResourcesOnDemand(windowEl) { try { // Check if already loaded if (typeof V86Backend !== 'undefined' && typeof V86Frontend !== 'undefined' && typeof V86Starter !== 'undefined') { console.log("V86 resources already loaded"); updateV86UI(); initializeV86WindowInstance(windowEl); console.log("V86 emulator window opened with lifecycle management"); return; } // Show loading progress in the window showV86LoadingProgress(windowEl); const resources = [ { src: 'v86/libv86.mjs', name: 'V86', label: 'Loading V86 Library...', type: 'module' }, { src: 'v86/v86-resources.js', name: 'V86ResourceManager', label: 'Loading V86 Resources...' }, { src: 'v86/v86-backend.js', name: 'V86Backend', label: 'Loading V86 Core...' }, { src: 'v86/v86-wrapper.js', name: 'V86Wrapper', label: 'Loading V86 Wrapper...' }, { src: 'v86/v86-frontend.js', name: 'V86Frontend', label: 'Loading V86 Frontend...' } ]; for (let i = 0; i < resources.length; i++) { const resource = resources[i]; updateV86LoadingProgress(windowEl, resource.label, (i / resources.length) * 100); if (typeof window[resource.name] === 'undefined') { await loadScriptWithProgress(resource.src, resource.name, resource.type === 'module'); } } // Final initialization updateV86LoadingProgress(windowEl, 'Initializing V86 Emulator...', 100); setTimeout(() => { hideV86LoadingProgress(windowEl); // Don't call updateV86UI here - let V86Frontend handle the interface initializeV86WindowInstance(windowEl); // Update V86 version in localStorage after successful loading if (window.v86CacheDetector && typeof window.v86CacheDetector.updateVersion === 'function') { window.v86CacheDetector.updateVersion(); } console.log("V86 emulator window opened with lifecycle management"); }, 500); } catch (error) { console.error("Failed to load V86 resources:", error); showV86LoadingError(windowEl, error.message); } } function showV86LoadingProgress(windowEl) { const container = windowEl.querySelector('.v86-container'); if (!container) return; // Add loading overlay without replacing the container content const overlay = document.createElement('div'); overlay.className = 'v86-loading-overlay'; overlay.innerHTML = `

Loading V86 Emulator

Preparing...
`; container.appendChild(overlay); } function updateV86LoadingProgress(windowEl, message, percentage) { const progressFill = windowEl.querySelector('.v86-progress-fill'); const progressText = windowEl.querySelector('.v86-progress-text'); if (progressFill) { progressFill.style.width = `${percentage}%`; } if (progressText) { progressText.textContent = message; } } function hideV86LoadingProgress(windowEl) { const overlay = windowEl.querySelector('.v86-loading-overlay'); if (overlay) { overlay.style.opacity = '0'; setTimeout(() => { // Don't regenerate the interface - let V86Frontend handle it // The V86Frontend.initialize() will create the proper interface overlay.remove(); }, 300); } } function showV86LoadingError(windowEl, errorMessage) { const container = windowEl.querySelector('.v86-container'); if (!container) return; container.innerHTML = `

Failed to Load V86 Emulator

${errorMessage}

`; } function retryV86Loading(button) { const windowEl = button.closest('.window'); if (windowEl) { loadV86ResourcesOnDemand(windowEl); } } function loadScriptWithProgress(src, expectedGlobal, isModule = false) { return new Promise(async (resolve, reject) => { // Handle ES modules differently if (isModule) { try { const cacheBuster = '?v=' + Date.now(); const module = await import('../' + src + cacheBuster); // For V86 module, expose it globally if (expectedGlobal === 'V86') { // V86 is the default export window[expectedGlobal] = module.default; } else if (expectedGlobal && module[expectedGlobal]) { window[expectedGlobal] = module[expectedGlobal]; } else if (expectedGlobal && module.default) { window[expectedGlobal] = module.default; } console.log(`Successfully loaded ${expectedGlobal || 'module'} from ${src}`); resolve(); } catch (error) { console.error(`Failed to load module ${src}:`, error); reject(error); } return; } // Regular script loading const script = document.createElement('script'); // Add cache-busting parameter const cacheBuster = '?v=' + Date.now(); script.src = src + cacheBuster; script.onload = function () { // Check for the global with retries let attempts = 0; const maxAttempts = 10; const checkGlobal = () => { attempts++; if (!expectedGlobal || typeof window[expectedGlobal] !== 'undefined') { console.log(`Successfully loaded ${expectedGlobal || 'script'} from ${src}`); resolve(); return; } if (attempts < maxAttempts) { console.log(`Attempt ${attempts}: ${expectedGlobal} not yet available, retrying...`); setTimeout(checkGlobal, 10); } else { console.error(`${expectedGlobal} not found after loading ${src}. Available V86 globals:`, Object.keys(window).filter(k => k.includes('V86'))); reject(new Error(`${expectedGlobal} not found after loading ${src}`)); } }; // Start checking immediately checkGlobal(); }; script.onerror = function () { reject(new Error(`Failed to load ${src}`)); }; document.head.appendChild(script); }); } async function loadV86AdditionalResources() { const resources = [ { src: 'v86/libv86.mjs', name: 'V86', type: 'module' }, { src: 'v86/v86-resources.js', name: 'V86ResourceManager' }, { src: 'v86/v86-backend.js', name: 'V86Backend' }, { src: 'v86/v86-wrapper.js', name: 'V86Wrapper' }, { src: 'v86/v86-frontend.js', name: 'V86Frontend' } ]; for (const resource of resources) { if (typeof window[resource.name] === 'undefined') { await loadScript(resource.src, resource.name); } } } function loadScript(src, expectedGlobal) { return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = src; script.onload = function () { if (expectedGlobal && typeof window[expectedGlobal] === 'undefined') { reject(new Error(`${expectedGlobal} not found after loading ${src}`)); } else { resolve(); } }; script.onerror = function () { reject(new Error(`Failed to load ${src}`)); }; document.head.appendChild(script); }); } function cleanupV86Resources() { // This function will be called when V86 emulator is uninstalled // It will clean up any running emulator instances and resources console.log("Cleaning up V86 emulator resources..."); try { // Close any open V86 emulator windows with proper cleanup Object.keys(windows).forEach(windowId => { if (windowId === 'v86-emulator' || windowId.startsWith('v86-emulator')) { const windowElement = windows[windowId]; if (windowElement) { // Trigger proper cleanup before closing handleV86WindowClose(windowElement); // Close the window const closeBtn = windowElement.querySelector('.window-btn.close'); if (closeBtn) { closeBtn.click(); } } } }); // Clean up all V86 frontend instances if (window.v86Instances) { Object.keys(window.v86Instances).forEach(instanceId => { const instance = window.v86Instances[instanceId]; if (instance && typeof instance.cleanup === 'function') { instance.cleanup(); } }); window.v86Instances = {}; } // Clean up global V86 backend instance if (v86Instance) { if (typeof v86Instance.cleanup === 'function') { v86Instance.cleanup(); } else if (typeof v86Instance.stop === 'function') { v86Instance.stop(); } v86Instance = null; } // Reset V86 state v86State = 'stopped'; // Clean up any V86-related timers if (window.v86WindowTimers) { Object.keys(window.v86WindowTimers).forEach(windowId => { window.v86WindowTimers[windowId].forEach(timerId => { clearTimeout(timerId); clearInterval(timerId); }); }); window.v86WindowTimers = {}; } // Clean up V86 resource manager cache if (typeof v86ResourceManager !== 'undefined' && v86ResourceManager.clearCache) { v86ResourceManager.clearCache(); } // Clean up V86 wrapper instances if (typeof v86Wrapper !== 'undefined' && v86Wrapper.destroyAllInstances) { v86Wrapper.destroyAllInstances(); } console.log("V86 emulator resources cleaned up successfully"); } catch (error) { console.error("Error during V86 resource cleanup:", error); showToast("Error cleaning up V86 resources", "fa-exclamation-triangle"); } } // V86 Emulator Interface Generation function generateV86Interface() { // Generate a unique container ID for this V86 instance const containerId = 'v86-container-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9); return `
V86 Emulator
Initializing...
`; } // V86 UI Update Function function updateV86UI() { console.log("Updating V86 UI components..."); try { // Update any V86-specific UI elements that need refreshing const v86Containers = document.querySelectorAll('.v86-container'); v86Containers.forEach(container => { // Check if container is still in initialization state const initializingDiv = container.querySelector('div[style*="Initializing"]'); if (initializingDiv) { // Update initialization message initializingDiv.textContent = 'Loading emulator components...'; } }); // Update V86 status indicators if they exist const statusElements = document.querySelectorAll('.v86-status-text'); statusElements.forEach(element => { if (element.textContent === 'Initializing...') { element.textContent = 'Loading resources...'; } }); console.log("V86 UI update completed"); } catch (error) { console.error("Error updating V86 UI:", error); } } // V86 Emulator Control Functions (Legacy - now handled by V86Frontend) // These functions are kept for backward compatibility but are no longer used let v86Instance = null; let v86State = 'stopped'; // V86 Window Lifecycle Management function handleV86WindowClose(windowElement) { console.log("Handling V86 window close - cleaning up emulator instance"); try { // Stop any running emulator instance if (v86State === 'running' && v86Instance) { stopV86Emulator(); } // Clean up any V86 frontend instances associated with this window const windowId = windowElement.dataset.windowId; if (windowId && window.v86Instances && window.v86Instances[windowId]) { const frontend = window.v86Instances[windowId]; if (frontend && typeof frontend.cleanup === 'function') { frontend.cleanup(); } delete window.v86Instances[windowId]; } // Clean up any event listeners or resources specific to this window cleanupV86WindowResources(windowElement); console.log("V86 window cleanup completed"); } catch (error) { console.error("Error during V86 window cleanup:", error); } } function handleV86WindowResize(windowElement) { console.log("Handling V86 window resize - adjusting display scaling"); try { // Get the window ID to find the associated frontend instance const windowId = windowElement.dataset.windowId; if (windowId && window.v86Instances && window.v86Instances[windowId]) { const frontend = window.v86Instances[windowId]; if (frontend && typeof frontend.updateScreenScale === 'function') { // Delay the scale update to allow window animation to complete setTimeout(() => { frontend.updateScreenScale(); }, 300); } } // Fallback: Update any V86 display elements in the window const v86Display = windowElement.querySelector('.v86-display'); if (v86Display) { // Trigger a resize event for any canvas elements const canvas = v86Display.querySelector('canvas'); if (canvas) { // Force canvas to recalculate its display size setTimeout(() => { const event = new Event('resize'); window.dispatchEvent(event); }, 300); } } } catch (error) { console.error("Error during V86 window resize handling:", error); } } function initializeV86WindowInstance(windowElement) { console.log("Initializing V86 window instance"); try { // Check memory limits before creating new instance if (!enforceV86MemoryLimits()) { showToast("Cannot create V86 instance: memory or instance limits exceeded", "fa-exclamation-triangle"); return; } // Generate a unique window ID const windowId = 'v86-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9); windowElement.dataset.windowId = windowId; // Initialize global V86 instances tracker if not exists if (!window.v86Instances) { window.v86Instances = {}; } // Initialize instance cache if not exists if (!window.v86InstanceCache) { window.v86InstanceCache = {}; } // Initialize window timers tracker if not exists if (!window.v86WindowTimers) { window.v86WindowTimers = {}; } window.v86WindowTimers[windowId] = []; // Find the V86 container in this window const v86Container = windowElement.querySelector('.v86-container'); if (v86Container) { // Get the container ID that was generated in generateV86Interface const containerId = v86Container.id; // Initialize V86 frontend for this window instance if (typeof V86Frontend !== 'undefined') { const frontend = new V86Frontend(v86Container); window.v86Instances[windowId] = frontend; // Initialize instance cache for this window window.v86InstanceCache[windowId] = { blobUrls: [], buffers: [], created: Date.now() }; // Initialize the frontend frontend.initialize().then(() => { console.log(`V86 frontend initialized for window ${windowId}`); // Log memory usage after initialization const usage = getV86MemoryUsage(); console.log(`V86 Memory Usage - Instances: ${usage.activeInstances}, Memory: ${Math.round(usage.totalMemoryAllocated / (1024 * 1024))}MB`); }).catch(error => { console.error(`Failed to initialize V86 frontend for window ${windowId}:`, error); // Clean up on initialization failure cleanupV86InstanceMemory(windowId); showToast("Failed to initialize V86 emulator", "fa-exclamation-triangle"); }); } else { console.error("V86Frontend class not available"); showToast("V86 emulator components not loaded", "fa-exclamation-triangle"); } } // Set up window-specific event handlers setupV86WindowEventHandlers(windowElement, windowId); console.log(`V86 window instance ${windowId} initialized`); } catch (error) { console.error("Error initializing V86 window instance:", error); showToast("Failed to initialize V86 emulator window", "fa-exclamation-triangle"); } } function setupV86WindowEventHandlers(windowElement, windowId) { // Handle window resize events const resizeObserver = new ResizeObserver(entries => { for (let entry of entries) { if (entry.target === windowElement) { handleV86WindowResize(windowElement); } } }); resizeObserver.observe(windowElement); // Store the observer for cleanup windowElement.v86ResizeObserver = resizeObserver; // Handle window focus events windowElement.addEventListener('mousedown', () => { // Ensure V86 input handling is properly managed when window gains focus const frontend = window.v86Instances && window.v86Instances[windowId]; if (frontend && frontend.inputFocused) { // Re-establish input focus if it was previously captured setTimeout(() => { if (frontend.canvas) { frontend.canvas.focus(); } }, 100); } }); } function cleanupV86WindowResources(windowElement) { try { console.log("Starting V86 window resource cleanup..."); // Clean up resize observer if (windowElement.v86ResizeObserver) { windowElement.v86ResizeObserver.disconnect(); delete windowElement.v86ResizeObserver; } // Clean up any other window-specific resources const windowId = windowElement.dataset.windowId; if (windowId) { // Remove any timers or intervals associated with this window if (window.v86WindowTimers && window.v86WindowTimers[windowId]) { window.v86WindowTimers[windowId].forEach(timerId => { clearTimeout(timerId); clearInterval(timerId); }); delete window.v86WindowTimers[windowId]; } // Clean up memory allocated for this instance cleanupV86InstanceMemory(windowId); } // Remove any event listeners attached to the window const events = ['mousedown', 'mousemove', 'mouseup', 'keydown', 'keyup', 'resize']; events.forEach(eventType => { windowElement.removeEventListener(eventType, windowElement[`v86${eventType}Handler`]); }); console.log("V86 window resources cleaned up successfully"); } catch (error) { console.error("Error cleaning up V86 window resources:", error); } } // V86 Memory Management for Multiple Instances function cleanupV86InstanceMemory(windowId) { try { console.log(`Cleaning up memory for V86 instance: ${windowId}`); // Clean up the specific frontend instance if (window.v86Instances && window.v86Instances[windowId]) { const instance = window.v86Instances[windowId]; // Call cleanup on the instance if (typeof instance.cleanup === 'function') { instance.cleanup(); } // Remove from instances registry delete window.v86Instances[windowId]; } // Clean up any cached resources specific to this instance if (window.v86InstanceCache && window.v86InstanceCache[windowId]) { const cache = window.v86InstanceCache[windowId]; // Clean up any blob URLs if (cache.blobUrls) { cache.blobUrls.forEach(url => { try { URL.revokeObjectURL(url); } catch (e) { console.warn("Failed to revoke blob URL:", url, e); } }); } // Clean up any array buffers if (cache.buffers) { cache.buffers.forEach(buffer => { // Clear the buffer (not strictly necessary but good practice) if (buffer instanceof ArrayBuffer) { // ArrayBuffers can't be explicitly freed, but we can clear references buffer = null; } }); } delete window.v86InstanceCache[windowId]; } // Force garbage collection hint (if available) if (window.gc && typeof window.gc === 'function') { try { window.gc(); } catch (e) { // gc() is not always available, ignore errors } } console.log(`Memory cleanup completed for V86 instance: ${windowId}`); } catch (error) { console.error(`Error cleaning up memory for V86 instance ${windowId}:`, error); } } function getV86MemoryUsage() { try { const usage = { activeInstances: 0, totalMemoryAllocated: 0, cacheSize: 0 }; // Count active instances if (window.v86Instances) { usage.activeInstances = Object.keys(window.v86Instances).length; // Calculate memory usage per instance Object.values(window.v86Instances).forEach(instance => { if (instance.config && instance.config.memory_size) { usage.totalMemoryAllocated += instance.config.memory_size; } }); } // Calculate cache size if (window.v86InstanceCache) { Object.values(window.v86InstanceCache).forEach(cache => { if (cache.buffers) { cache.buffers.forEach(buffer => { if (buffer instanceof ArrayBuffer) { usage.cacheSize += buffer.byteLength; } }); } }); } return usage; } catch (error) { console.error("Error calculating V86 memory usage:", error); return { activeInstances: 0, totalMemoryAllocated: 0, cacheSize: 0 }; } } function enforceV86MemoryLimits() { try { const usage = getV86MemoryUsage(); const maxInstances = 3; // Limit to 3 concurrent instances const maxTotalMemory = 1024 * 1024 * 1024; // 1GB total limit if (usage.activeInstances > maxInstances) { console.warn(`V86 instance limit exceeded: ${usage.activeInstances}/${maxInstances}`); showToast(`Too many V86 instances running (${usage.activeInstances}/${maxInstances}). Consider closing some.`, "fa-exclamation-triangle"); return false; } if (usage.totalMemoryAllocated > maxTotalMemory) { console.warn(`V86 memory limit exceeded: ${Math.round(usage.totalMemoryAllocated / (1024 * 1024))}MB`); showToast("V86 memory usage is high. Consider reducing memory allocation or closing instances.", "fa-exclamation-triangle"); return false; } return true; } catch (error) { console.error("Error enforcing V86 memory limits:", error); return true; // Allow operation to continue on error } } // V86 Error Handling and User Feedback function handleV86LoadError(errorType, errorMessage) { console.error(`V86 Load Error [${errorType}]:`, errorMessage); const errorHandlers = { 'BACKEND_LOAD_FAILED': () => { showV86ErrorDialog( 'Backend Loading Failed', 'The V86 emulator core could not be loaded. This might be due to:', [ 'Network connectivity issues', 'Missing or corrupted emulator files', 'Browser compatibility problems' ], [ { text: 'Retry', action: () => retryV86Load() }, { text: 'Check Network', action: () => window.open('https://www.google.com', '_blank') }, { text: 'Close', action: () => closeV86ErrorDialog() } ] ); }, 'LOAD_TIMEOUT': () => { showV86ErrorDialog( 'Loading Timeout', 'The V86 emulator took too long to load. This might be due to:', [ 'Slow internet connection', 'Large emulator files', 'Server response issues' ], [ { text: 'Retry', action: () => retryV86Load() }, { text: 'Close', action: () => closeV86ErrorDialog() } ] ); }, 'RESOURCE_NOT_FOUND': () => { showV86ErrorDialog( 'Resources Missing', 'Required V86 emulator files are missing:', [ 'BIOS files may not be available', 'WebAssembly core is missing', 'Installation may be incomplete' ], [ { text: 'Reinstall', action: () => reinstallV86() }, { text: 'Close', action: () => closeV86ErrorDialog() } ] ); }, 'INITIALIZATION_FAILED': () => { showV86ErrorDialog( 'Initialization Failed', 'The V86 emulator could not be initialized:', [ 'Insufficient memory available', 'WebAssembly not supported', 'Configuration errors' ], [ { text: 'Reduce Memory', action: () => showV86MemorySettings() }, { text: 'Retry', action: () => retryV86Initialization() }, { text: 'Close', action: () => closeV86ErrorDialog() } ] ); }, 'RUNTIME_ERROR': () => { showV86ErrorDialog( 'Runtime Error', 'The V86 emulator encountered an error during operation:', [ 'Emulation crashed unexpectedly', 'Memory allocation failed', 'Invalid disk image or configuration' ], [ { text: 'Restart Emulator', action: () => restartV86Emulator() }, { text: 'Reset Configuration', action: () => resetV86Configuration() }, { text: 'Close', action: () => closeV86ErrorDialog() } ] ); }, 'UNEXPECTED_ERROR': () => { showV86ErrorDialog( 'Unexpected Error', 'An unexpected error occurred:', [errorMessage || 'Unknown error'], [ { text: 'Report Issue', action: () => reportV86Issue(errorType, errorMessage) }, { text: 'Retry', action: () => retryV86Load() }, { text: 'Close', action: () => closeV86ErrorDialog() } ] ); } }; const handler = errorHandlers[errorType] || errorHandlers['UNEXPECTED_ERROR']; handler(); } function showV86ErrorDialog(title, description, causes, actions) { const dialog = document.createElement('div'); dialog.className = 'v86-error-dialog'; dialog.innerHTML = `

${title}

${description}

    ${causes.map(cause => `
  • ${cause}
  • `).join('')}
${actions.map(action => `` ).join('')}
`; // Add styles const style = document.createElement('style'); style.textContent = ` .v86-error-dialog { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 10000; } .v86-error-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); display: flex; align-items: center; justify-content: center; } .v86-error-content { background: var(--bg-primary); border: 1px solid var(--border); border-radius: 12px; max-width: 500px; width: 90%; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); } .v86-error-header { display: flex; align-items: center; gap: 10px; padding: 20px; border-bottom: 1px solid var(--border); color: var(--error-red); } .v86-error-header i { font-size: 24px; } .v86-error-header h3 { margin: 0; color: var(--text-primary); } .v86-error-body { padding: 20px; } .v86-error-body p { margin: 0 0 15px 0; color: var(--text-primary); } .v86-error-causes { margin: 0; padding-left: 20px; color: var(--text-secondary); } .v86-error-causes li { margin-bottom: 5px; } .v86-error-actions { display: flex; gap: 10px; padding: 20px; border-top: 1px solid var(--border); justify-content: flex-end; } .v86-error-btn { padding: 8px 16px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg-secondary); color: var(--text-primary); cursor: pointer; transition: all 0.2s; } .v86-error-btn:hover { background: var(--accent); color: white; } `; document.head.appendChild(style); document.body.appendChild(dialog); // Store references for cleanup dialog.v86Style = style; window.currentV86ErrorDialog = dialog; // Set up action handlers actions.forEach((action, index) => { const btn = dialog.querySelectorAll('.v86-error-btn')[index]; btn.onclick = action.action; }); } function closeV86ErrorDialog() { if (window.currentV86ErrorDialog) { if (window.currentV86ErrorDialog.v86Style) { window.currentV86ErrorDialog.v86Style.remove(); } window.currentV86ErrorDialog.remove(); window.currentV86ErrorDialog = null; } } function retryV86Load() { closeV86ErrorDialog(); showToast("Retrying V86 emulator load...", "fa-redo"); loadV86Resources().catch(error => { console.error("Retry failed:", error); }); } function reinstallV86() { closeV86ErrorDialog(); showToast("Reinstalling V86 emulator...", "fa-download"); // Clean up existing resources cleanupV86Resources(); // Remove from installed apps and reinstall const index = installedApps.indexOf('v86-emulator'); if (index > -1) { installedApps.splice(index, 1); localStorage.setItem('nebulo_installedApps', JSON.stringify(installedApps)); } // Trigger reinstallation setTimeout(() => { installApp('v86-emulator'); }, 1000); } function retryV86Initialization() { closeV86ErrorDialog(); showToast("Retrying V86 initialization...", "fa-redo"); // Find the current V86 window and reinitialize const v86Window = windows['v86-emulator']; if (v86Window) { const windowId = v86Window.dataset.windowId; if (windowId && window.v86Instances && window.v86Instances[windowId]) { const frontend = window.v86Instances[windowId]; frontend.initialize().catch(error => { handleV86LoadError('INITIALIZATION_FAILED', error.message); }); } } } function showV86MemorySettings() { closeV86ErrorDialog(); showToast("Opening memory settings...", "fa-memory"); // Show V86 configuration panel if available const configPanel = document.getElementById('v86Config'); if (configPanel) { configPanel.style.display = 'block'; } } function restartV86Emulator() { closeV86ErrorDialog(); showToast("Restarting V86 emulator...", "fa-redo"); if (v86State === 'running') { stopV86Emulator(); setTimeout(() => { startV86Emulator(); }, 2000); } } function resetV86Configuration() { closeV86ErrorDialog(); showToast("Resetting V86 configuration...", "fa-cog"); // Reset memory and boot order to defaults const memorySelect = document.getElementById('v86Memory'); const bootOrderSelect = document.getElementById('v86BootOrder'); if (memorySelect) memorySelect.value = '128'; if (bootOrderSelect) bootOrderSelect.value = '0x213'; showToast("V86 configuration reset to defaults", "fa-check-circle"); } function reportV86Issue(errorType, errorMessage) { closeV86ErrorDialog(); const issueData = { type: errorType, message: errorMessage, timestamp: new Date().toISOString(), userAgent: navigator.userAgent, url: window.location.href }; console.log("V86 Issue Report:", issueData); showToast("Issue details logged to console", "fa-bug"); } // ==================== WebLLM AI Assistant ==================== let nautilusAI = { engine: null, messages: [], isInitialized: false, isInitializing: false, isGenerating: false, pendingTools: [], toolsEnabled: true, actionLog: [], currentModel: 'smart' // 'fast' or 'smart' - default to smart (Qwen3 with thinking) }; const AI_MODELS = { fast: { name: 'SmolLM2-360M-Instruct-q4f16_1-MLC', label: 'Dumb', description: 'Quick responses, less accurate', icon: 'fa-bolt' }, smart: { name: 'Qwen3-0.6B-q4f16_1-MLC', label: 'Smarty', description: 'Advanced reasoning with thinking mode', icon: 'fa-brain' } }; // OS Automation Tools - Available for AI to control the OS const OS_AUTOMATION_TOOLS = { open_app: { name: "open_app", description: "Open an application in Nebulo", parameters: { app_name: "string - Name of the app (e.g., 'files', 'terminal', 'settings', 'browser', 'editor', 'music', 'photos', 'calculator', 'appstore', 'cloaking', 'achievements', 'ai-snake')", reason: "string - Why this app needs to be opened" } }, close_app: { name: "close_app", description: "Close a currently open application window", parameters: { app_name: "string - Name of the app to close", reason: "string - Why this app needs to be closed" } }, run_terminal_command: { name: "run_terminal_command", description: "Execute a terminal command in Nebulo", parameters: { command: "string - The terminal command to execute (e.g., 'ls', 'apps', 'help', 'date', 'whoami', 'clear')", reason: "string - Why this command needs to be executed" } }, create_file: { name: "create_file", description: "Create a new file in the file system", parameters: { path: "string - Full path including filename (e.g., '/documents/note.txt')", content: "string - Content to write to the file", reason: "string - Why this file needs to be created" } }, read_file: { name: "read_file", description: "Read the contents of a file", parameters: { path: "string - Full path to the file to read", reason: "string - Why this file needs to be read" } }, create_folder: { name: "create_folder", description: "Create a new folder in the file system", parameters: { path: "string - Full path of the folder to create", reason: "string - Why this folder needs to be created" } }, delete_item: { name: "delete_item", description: "Delete a file or folder", parameters: { path: "string - Full path to the item to delete", reason: "string - Why this item needs to be deleted" } }, take_screenshot: { name: "take_screenshot", description: "Capture a screenshot of the desktop", parameters: { reason: "string - Why a screenshot is needed" } }, change_setting: { name: "change_setting", description: "Change a system setting", parameters: { setting: "string - The setting to change (e.g., 'theme')", value: "string - The new value for the setting", reason: "string - Why this setting needs to be changed" } }, list_apps: { name: "list_apps", description: "List all installed applications", parameters: { reason: "string - Why you need to list apps" } }, list_files: { name: "list_files", description: "List files and folders in a directory", parameters: { path: "string - Directory path to list (default: '/')", reason: "string - Why you need to list this directory" } }, get_system_info: { name: "get_system_info", description: "Get current system information (open apps, username, uptime, etc.)", parameters: { reason: "string - Why you need system information" } }, get_available_options: { name: "get_available_options", description: "Get available options for settings (themes, search engines, etc.) - USE THIS FIRST before changing settings!", parameters: { category: "string - Category to query: 'themes', 'apps', 'search_engines', 'settings', 'all'", reason: "string - Why you need this information" } } }; const NAUTILUS_SYSTEM_PROMPT = `You are the Nebulo AI Assistant, an expert guide for Nebulo - a web-based operating system built entirely in HTML, CSS, and JavaScript. OS AUTOMATION CAPABILITIES: You have the ability to control and automate Nebulo! You can execute actions on behalf of the user with their approval. IMPORTANT: When you want to perform an action, output a JSON object in your response with this structure: PREFERRED FORMAT (with XML tags): { "tool": "tool_name_here", "parameters": { "param1": "value1", "param2": "value2" } } ALTERNATIVE FORMAT (bare JSON also works): {"tool": "tool_name_here", "parameters": {"param1": "value1", "param2": "value2"}} Both formats are supported. The system will detect and execute your tool calls automatically. Available tools: ${Object.keys(OS_AUTOMATION_TOOLS).map(toolName => { const tool = OS_AUTOMATION_TOOLS[toolName]; return `- ${tool.name}: ${tool.description}\n Parameters: ${JSON.stringify(tool.parameters, null, 2)}`; }).join('\n\n')} TOOL USAGE GUIDELINES: 1. Always explain what you're about to do BEFORE the tool call 2. **IMPORTANT: Use get_available_options FIRST before changing settings!** Query available themes, apps, search engines, etc. 3. Use tool calls when they would be helpful (e.g., if user asks to open an app, use open_app) 4. You can make multiple tool calls in one response if needed 5. Always provide a clear "reason" parameter explaining why the action is needed 6. After making a tool call, explain what you did and what the result was 7. The user must approve each action - they will see a confirmation dialog 8. Tool results will be provided to you in a follow-up message CRITICAL FOR SETTINGS: - Before changing theme → use get_available_options with category='themes' to see installed themes - Before changing search engine → use get_available_options with category='search_engines' - For other settings → use get_available_options with category='settings' - This prevents errors from using invalid values! EXAMPLE CONVERSATIONS: Example 1 - Opening an app: User: "Can you open the calculator app for me?" You: "I'll open the Calculator app for you right away! { "tool": "open_app", "parameters": { "app_name": "calculator", "reason": "User requested to open the calculator" } } The calculator should now be opening on your screen." Example 2 - Changing theme (CORRECT way): User: "Change the theme to blue" You: "I'll check what themes are available and change it to blue! { "tool": "get_available_options", "parameters": { "category": "themes", "reason": "Need to verify blue theme is installed before changing" } } Once I confirm the blue theme is installed, I'll change it for you." [After getting results showing blue is installed] { "tool": "change_setting", "parameters": { "setting": "theme", "value": "blue", "reason": "User requested blue theme, confirmed it's installed" } } Example 3 - Multiple actions: User: "Open terminal and run the date command" You: "I'll open the terminal and run the date command! { "tool": "open_app", "parameters": { "app_name": "terminal", "reason": "Need terminal open to run command" } } { "tool": "run_terminal_command", "parameters": { "command": "date", "reason": "User wants to see current date" } } " CORE KNOWLEDGE ABOUT NAUTILUSOS: ARCHITECTURE & TECHNOLOGY: - Nebulo is a complete web-based OS running entirely in the browser - Built with vanilla HTML, CSS, and JavaScript (no frameworks) - Uses local storage for persistence (localStorage and sessionStorage) - File system is virtual and stored in browser memory/localStorage - All windows, apps, and features are DOM-based and fully interactive - Supports multiple themes, customization, and user profiles AVAILABLE APPLICATIONS: 1. Files - Full file explorer with folder navigation, tree sidebar, create/delete/rename operations 2. Terminal - Command-line interface with bash-like commands 3. Browser - Built-in Scramjet-powered web browser 4. Text Editor - Create and edit text files with save/load functionality 5. Music Player - Play music from URLs or uploaded files 6. Photos - View, upload, and manage photos with screenshot capability 7. Settings - Customize themes, time format, desktop icons, wallpapers, cloaking 8. Calculator - Standard calculator with history 9. Help - Comprehensive documentation and keyboard shortcuts 10. What's New - Feature showcase carousel with illustrations 11. App Store - Install themes, apps, and games 12. Achievements - Track progress and unlock badges 13. Cloaking - Tab disguise and panic key features 14. Task Manager - View and manage running applications 15. Snap Manager - Configure window snapping layouts and keybinds 16. Startup Apps - Configure apps to launch on boot 17. V86 Emulator - Run actual operating systems in the browser 18. AI Snake Learning - Neural network that learns to play Snake (by lanefiedler-731) 19. Nebulo AI Assistant - That's me! Your helpful AI guide (by lanefiedler-731) WINDOW MANAGEMENT: - Fully draggable windows with title bar drag - Resizable from any edge or corner - Minimize, maximize, and close buttons - Focus management with visual indicators - Windows appear in taskbar when open - Snap-to-edge functionality (configurable in Snap Manager) - Multiple windows can be open simultaneously FILE SYSTEM: - Virtual hierarchical file system - Default folders: Photos, TextEditor, Documents, etc. - Create folders and files through Files app or Text Editor - Files are stored as objects in localStorage - Photos stored as blob URLs - File operations: create, delete, rename, move, copy - Tree view sidebar for easy navigation CUSTOMIZATION & THEMING: Available themes: Dark (default), Light, Golden, Red, Blue, Purple, Green, Liquid Glass - Install themes from App Store - Apply themes in Settings - Custom wallpapers for desktop and login screen - Profile pictures - Desktop icon visibility toggle - Clock format (12h/24h, with/without seconds) - Accent colors vary by theme BOOT & LOGIN: - Bootloader with two options: GUI mode or Command Line mode - Account setup wizard on first launch - Username and password support (or passwordless) - Login screen with clock and system info - Boot preference saved to localStorage - Uptime tracking from boot time TASKBAR FEATURES: - Start menu with app grid and user info - Running app indicators - Sign out / shut down buttons - Quick actions panel (screenshots, close all windows) - System clock - Notification center - Search apps functionality SETTINGS CATEGORIES: 1. Appearance - Themes, wallpapers, desktop icons 2. Time & Date - Clock format, seconds display 3. Account - Username, profile picture, password 4. Cloaking - Tab disguise, panic keys, auto-rotation 5. Import/Export - Backup and restore profiles 6. Advanced - Reset data, developer options CLOAKING & SECURITY: - Tab disguise to mimic other websites (Google, Drive, Classroom, etc.) - Custom tab titles and favicons - Panic key support for instant redirects - Auto-rotate disguises at configurable intervals - Helps bypass school/workplace restrictions ACHIEVEMENTS SYSTEM: - Unlock badges for milestones - Categories: Productivity, Exploration, Time-Based, Easter Eggs - Persistent across data resets - Examples: First File Created, Theme Changer, Early Bird, Night Owl KEYBOARD SHORTCUTS: - Snap windows: Ctrl+Alt+Arrow Keys - Quick app launch from Start menu search - Focus windows with taskbar clicks - Context menus with right-click BROWSER FEATURES: - Embedded Helios Browser with full proxy support - Multiple tabs support - URL navigation - Back/forward buttons - Tab cloaking integration TERMINAL COMMANDS: Common commands: ls, cd, mkdir, rm, cat, echo, clear, help, pwd, whoami - Bash-like syntax and behavior - File system integration - Command history TECHNICAL DETAILS: - Uses CSS variables for theming - FontAwesome icons throughout - Smooth animations and transitions - Responsive to different screen sizes - Service Worker registration for offline support - LocalStorage keys prefixed with "nebulo_" - File protocol warning for features requiring web server APP STORE: - Install additional themes beyond default dark theme - Games: Tic-Tac-Toe, AI Snake Learning - Apps: V86 Emulator, Task Manager, Snap Manager, Startup Apps - Each item shows install status and description EXPORT/IMPORT PROFILES: - Export entire system state to JSON file - Includes: files, settings, themes, installed apps, wallpapers - Import profile to restore on any device - Great for backups or sharing configurations ADVANCED FEATURES: - Window snapping with customizable layouts - Startup apps run automatically on boot - Task manager shows resource usage - Screenshots using browser screen capture API - Drag-and-drop file uploads for photos - Context menus for desktop and file operations DEVELOPER INFO: - Nebulo created by lanefiedler-731, x8r, and PrismX - Open source on GitHub - No external frameworks (vanilla JS) - Community contributions welcome - Apps can be created by community (Nebulo AI Assistant by lanefiedler-731) THINKING MODE (QWEN3 CAPABILITY): You have advanced reasoning capabilities! For complex tasks, you can use thinking mode to show your reasoning process: - Wrap your internal reasoning in tags - Use thinking for: complex calculations, multi-step logic, planning, analysis, debugging - Think through problems step-by-step before giving your final answer - Thinking content will be displayed separately in a collapsible section - Your final response (outside tags) should be clear and concise Example: Let me analyze the user's request: 1. They want to change the theme 2. I need to check available themes first using get_available_options 3. Then I'll change the setting if the theme exists 4. This requires two tool calls in sequence I'll help you change the theme! First, let me check what themes are available... When helping users: 1. Be specific and detailed about features 2. Provide step-by-step instructions when needed 3. Reference exact app names and menu locations 4. Explain both basic and advanced features 5. Suggest related features users might find helpful 6. Be enthusiastic about Nebulo capabilities 7. If unsure about something, admit it honestly 8. Use thinking mode for complex reasoning and problem-solving Your goal is to make users feel confident and excited about using Nebulo!`; async function initializeNebuloAI(modelKey = null) { if (nautilusAI.isInitializing) return; // Allow re-initialization with different model const isModelSwitch = nautilusAI.isInitialized && modelKey !== null; if (nautilusAI.isInitialized && !isModelSwitch) return; nautilusAI.isInitializing = true; const statusEl = document.getElementById('aiStatus'); const sendBtn = document.getElementById('aiSendBtn'); const inputEl = document.getElementById('aiInput'); try { // Use selected model or current model const selectedModel = modelKey || nautilusAI.currentModel; const modelConfig = AI_MODELS[selectedModel]; updateAiStatus(`Loading WebLLM library...`, true); // Load WebLLM from CDN with ES modules support (only once) if (!window.mlcai) { await new Promise((resolve, reject) => { const script = document.createElement('script'); script.type = 'module'; script.textContent = ` import * as webllm from "https://esm.run/@mlc-ai/web-llm"; window.mlcai = webllm; `; script.onload = resolve; script.onerror = () => reject(new Error('Failed to load WebLLM library')); document.head.appendChild(script); // Wait for window.mlcai to be available const checkInterval = setInterval(() => { if (window.mlcai) { clearInterval(checkInterval); resolve(); } }, 50); // Reduced from 100ms for faster check // Timeout after 10 seconds setTimeout(() => { clearInterval(checkInterval); if (!window.mlcai) { reject(new Error('WebLLM library load timeout')); } }, 10000); }); } updateAiStatus(`Initializing ${modelConfig.label} model...`, true); const { CreateMLCEngine } = window.mlcai; // Check WebGPU toggle (default to true for speed) const useWebGPU = document.getElementById('aiWebGPUToggle')?.checked !== false; // Dispose old engine if switching models if (isModelSwitch && nautilusAI.engine) { try { await nautilusAI.engine.unload(); } catch (e) { console.warn('Error unloading previous model:', e); } } // Create engine with optimizations for faster first token nautilusAI.engine = await CreateMLCEngine(modelConfig.name, { initProgressCallback: (progress) => { updateAiStatus(`Loading ${modelConfig.label}: ${Math.round(progress.progress * 100)}%`, true); }, useWebGPU: useWebGPU, // Optimizations for faster initialization and TTFT logLevel: 'ERROR', // Reduce logging overhead temperature: 0.7, // Optimal for thinking mode top_p: 0.95, // Enable prompt caching for faster subsequent requests caching: true }); // Reset or preserve conversation history if (isModelSwitch) { // Keep conversation history when switching models const oldMessages = nautilusAI.messages.filter(m => m.role !== 'system'); nautilusAI.messages = [ { role: "system", content: NAUTILUS_SYSTEM_PROMPT }, ...oldMessages ]; } else { // Fresh start nautilusAI.messages = [ { role: "system", content: NAUTILUS_SYSTEM_PROMPT } ]; } nautilusAI.currentModel = selectedModel; nautilusAI.isInitialized = true; nautilusAI.isInitializing = false; updateAiStatus(`${modelConfig.label} Ready`, false); if (sendBtn) sendBtn.disabled = false; if (inputEl) { inputEl.disabled = false; inputEl.focus(); } if (isModelSwitch) { showToast(`Switched to ${modelConfig.label}`, modelConfig.icon); } else { showToast(`${modelConfig.label} is ready!`, 'fa-robot'); } } catch (error) { console.error('Failed to initialize Nebulo AI:', error); nautilusAI.isInitializing = false; updateAiStatus(`Error: ${error.message}`, false, true); showToast('Failed to load AI model. Check console for details.', 'fa-exclamation-triangle'); // Re-enable input so user can try again if (sendBtn) sendBtn.disabled = false; if (inputEl) inputEl.disabled = false; } } async function switchAIModel(modelKey) { if (nautilusAI.isGenerating) { showToast('Cannot switch models while generating response', 'fa-exclamation-circle'); // Reset select to current model const select = document.getElementById('aiModelSelect'); if (select) select.value = nautilusAI.currentModel; return; } if (modelKey === nautilusAI.currentModel) return; const modelConfig = AI_MODELS[modelKey]; updateAiStatus(`Switching to ${modelConfig.label}...`, true); nautilusAI.isInitialized = false; await initializeNebuloAI(modelKey); } function updateAiStatus(message, loading = false, error = false) { const statusEl = document.getElementById('aiStatus'); if (!statusEl) return; const color = error ? '#ef4444' : (loading ? '#64748b' : 'var(--accent)'); const spinner = loading ? `
` : ''; statusEl.innerHTML = `
${spinner} ${message}
`; } async function sendAiMessage() { const inputEl = document.getElementById('aiInput'); const sendBtn = document.getElementById('aiSendBtn'); if (!inputEl || !sendBtn) return; const userMessage = inputEl.value.trim(); if (!userMessage) return; if (!nautilusAI.isInitialized) { await initializeNebuloAI(); if (!nautilusAI.isInitialized) return; } if (nautilusAI.isGenerating) { showToast('Please wait for the current response to complete', 'fa-hourglass-half'); return; } // Add user message to chat addAiChatMessage(userMessage, true); inputEl.value = ''; inputEl.style.height = 'auto'; // Disable input while generating nautilusAI.isGenerating = true; sendBtn.disabled = true; inputEl.disabled = true; updateAiStatus('Thinking...', true); try { // Add user message to conversation history nautilusAI.messages.push({ role: "user", content: userMessage }); // Create placeholder message for streaming const chatContainer = document.getElementById('aiChatMessages'); const messageDiv = document.createElement('div'); messageDiv.style.cssText = ` background: rgba(125, 211, 192, 0.1); border: 1px solid rgba(125, 211, 192, 0.3); border-radius: 10px; padding: 15px; animation: slideIn 0.3s ease-out; `; const headerDiv = document.createElement('div'); headerDiv.style.cssText = 'display: flex; align-items: center; gap: 8px; margin-bottom: 8px;'; headerDiv.innerHTML = ` Nebulo AI `; const contentDiv = document.createElement('div'); contentDiv.style.cssText = 'color: #cbd5e1; line-height: 1.6;'; const textDiv = document.createElement('div'); textDiv.className = 'ai-response-text'; textDiv.style.cssText = 'white-space: pre-wrap; word-wrap: break-word;'; contentDiv.appendChild(textDiv); messageDiv.appendChild(headerDiv); messageDiv.appendChild(contentDiv); chatContainer.appendChild(messageDiv); chatContainer.scrollTop = chatContainer.scrollHeight; // Generate response with streaming - optimized settings based on model const isFastModel = nautilusAI.currentModel === 'fast'; const isSmartModel = nautilusAI.currentModel === 'smart'; const completionConfig = { messages: nautilusAI.messages, temperature: isFastModel ? 0.5 : 0.7, // Lower temp for faster model = more focused max_tokens: isFastModel ? 2048 : 4096, // Fewer tokens for fast model stream: true, // Optimizations for faster first token top_p: isFastModel ? 0.9 : 0.95, frequency_penalty: 0, presence_penalty: 0, // Enable streaming for lower latency stream_options: { include_usage: false } }; // Enable thinking mode for smart model (Qwen3) if (isSmartModel) { completionConfig.extra_body = { enable_thinking: true, // Additional TTFT optimizations use_beam_search: false, early_stopping: false }; } const completion = await nautilusAI.engine.chat.completions.create(completionConfig); let fullResponse = ''; let thinkingSection = null; let thinkContentDiv = null; let thinkingSummary = null; for await (const chunk of completion) { const delta = chunk.choices[0]?.delta?.content || ''; if (!delta) continue; fullResponse += delta; const thinkStartIndex = fullResponse.indexOf(''); const thinkEndIndex = fullResponse.indexOf(''); let visibleContent = fullResponse; let currentThinking = ''; if (thinkStartIndex !== -1) { if (!thinkingSection) { thinkingSection = document.createElement('details'); thinkingSection.open = true; thinkingSection.style.cssText = 'margin: 0 0 15px 0; padding: 12px; background: rgba(125, 211, 192, 0.08); border: 1px solid rgba(125, 211, 192, 0.25); border-radius: 8px; order: -1;'; thinkingSummary = document.createElement('summary'); thinkingSummary.style.cssText = 'cursor: pointer; color: var(--accent); font-weight: bold; user-select: none; list-style: none; display: flex; align-items: center; gap: 8px;'; thinkingSummary.innerHTML = ' Thinking'; thinkingSummary.addEventListener('click', (e) => { e.preventDefault(); thinkingSection.open = !thinkingSection.open; }); thinkContentDiv = document.createElement('div'); thinkContentDiv.style.cssText = 'margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(125, 211, 192, 0.15); color: #94a3b8; font-style: italic; white-space: pre-wrap; line-height: 1.6;'; thinkContentDiv.className = 'thinking-content'; thinkingSection.appendChild(thinkingSummary); thinkingSection.appendChild(thinkContentDiv); contentDiv.style.display = 'flex'; contentDiv.style.flexDirection = 'column'; contentDiv.insertBefore(thinkingSection, textDiv); if (!document.getElementById('thinking-dots-style')) { const style = document.createElement('style'); style.id = 'thinking-dots-style'; style.textContent = ` @keyframes thinkingDots { 0% { content: ''; } 25% { content: '.'; } 50% { content: '..'; } 75% { content: '...'; } 100% { content: ''; } } .thinking-dots::after { content: ''; animation: thinkingDots 1.5s infinite; } summary::-webkit-details-marker { display: none; } `; document.head.appendChild(style); } } if (thinkEndIndex === -1) { currentThinking = fullResponse.substring(thinkStartIndex + 7); visibleContent = fullResponse.substring(0, thinkStartIndex); } else { currentThinking = fullResponse.substring(thinkStartIndex + 7, thinkEndIndex); visibleContent = fullResponse.substring(0, thinkStartIndex) + fullResponse.substring(thinkEndIndex + 8); if (thinkingSummary) { thinkingSummary.innerHTML = ' Thought'; } } if (thinkContentDiv) { thinkContentDiv.textContent = currentThinking; } } textDiv.textContent = visibleContent; chatContainer.scrollTop = chatContainer.scrollHeight; } // Add AI response to conversation history nautilusAI.messages.push({ role: "assistant", content: fullResponse }); // Process tool calls if any const toolCalls = parseToolCalls(fullResponse); if (toolCalls.length > 0 && nautilusAI.toolsEnabled) { updateAiStatus('Processing tool calls...', true); // Remove tool call tags from visible text const cleanedText = removeToolCallsFromText(fullResponse); textDiv.textContent = cleanedText; // Process each tool call with user approval for (const toolCall of toolCalls) { await new Promise((resolve) => { showToolApprovalDialog( toolCall, async () => { // User approved - execute the tool updateAiStatus(`Executing: ${toolCall.tool}...`, true); const result = await executeToolCall(toolCall); addToolFeedbackToChat(toolCall, result); // Add tool result to conversation for AI context nautilusAI.messages.push({ role: "user", content: `[Tool Execution Result]\nTool: ${toolCall.tool}\nSuccess: ${result.success}\nMessage: ${result.message}\n${result.details ? 'Details: ' + result.details : ''}` }); resolve(); }, () => { // User rejected - show feedback addToolFeedbackToChat(toolCall, { success: false, message: `⛔ Action rejected by user`, details: `The ${toolCall.tool} action was not executed.` }); // Inform AI that tool was rejected nautilusAI.messages.push({ role: "user", content: `[Tool Execution Result]\nTool: ${toolCall.tool}\nSuccess: false\nMessage: User rejected this action` }); resolve(); } ); }); } } updateAiStatus('AI Assistant Ready', false); } catch (error) { console.error('AI generation error:', error); addAiChatMessage('Sorry, I encountered an error while generating a response. Please try again.', false, true); updateAiStatus('Error occurred', false, true); } finally { nautilusAI.isGenerating = false; sendBtn.disabled = false; inputEl.disabled = false; inputEl.focus(); } } function addAiChatMessage(message, isUser = false, isError = false) { const chatContainer = document.getElementById('aiChatMessages'); if (!chatContainer) return; const messageDiv = document.createElement('div'); messageDiv.style.cssText = ` background: ${isUser ? 'rgba(125, 211, 192, 0.15)' : (isError ? 'rgba(239, 68, 68, 0.15)' : 'rgba(125, 211, 192, 0.1)')}; border: 1px solid ${isUser ? 'rgba(125, 211, 192, 0.4)' : (isError ? 'rgba(239, 68, 68, 0.4)' : 'rgba(125, 211, 192, 0.3)')}; border-radius: 10px; padding: 15px; animation: slideIn 0.3s ease-out; `; const headerDiv = document.createElement('div'); headerDiv.style.cssText = 'display: flex; align-items: center; gap: 8px; margin-bottom: 8px;'; headerDiv.innerHTML = ` ${isUser ? 'You' : 'Nebulo AI'} `; const contentDiv = document.createElement('div'); contentDiv.style.cssText = 'color: #cbd5e1; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;'; contentDiv.textContent = message; messageDiv.appendChild(headerDiv); messageDiv.appendChild(contentDiv); chatContainer.appendChild(messageDiv); // Scroll to bottom chatContainer.scrollTop = chatContainer.scrollHeight; } function sendQuickQuestion(question) { const inputEl = document.getElementById('aiInput'); if (!inputEl) return; inputEl.value = question; sendAiMessage(); } function clearAiChat() { const chatContainer = document.getElementById('aiChatMessages'); if (!chatContainer) return; // Clear all messages except the welcome message const currentModelConfig = AI_MODELS[nautilusAI.currentModel] || AI_MODELS['smart']; chatContainer.innerHTML = `
Nebulo AI
Hello! I'm your Nebulo AI Assistant. I can help you understand and navigate Nebulo.

Using: ${currentModelConfig.label} - ${currentModelConfig.description}

 OS Automation Enabled!
I can control Nebulo for you! Just ask me to do something and I'll request your approval before taking action.

Thinking Mode Active!
I can show my reasoning process for complex tasks. Watch for the collapsible "Thinking" section!

Try: "Open calculator" or "List available themes"
`; // Reset conversation history (keep system prompt) nautilusAI.messages = [ { role: "system", content: NAUTILUS_SYSTEM_PROMPT } ]; showToast('Chat cleared', 'fa-trash'); } // ==================== OS Automation Tool Execution ==================== function parseToolCalls(text) { const toolCalls = []; // Method 1: Try to find tool calls wrapped in XML tags (preferred format) const xmlRegex = /([\s\S]*?)<\/tool_call>/g; let match; while ((match = xmlRegex.exec(text)) !== null) { try { const toolData = JSON.parse(match[1].trim()); if (toolData.tool && toolData.parameters) { toolCalls.push(toolData); } } catch (e) { console.error('Failed to parse XML-wrapped tool call:', e); } } // Method 2: If no XML tags found, look for bare JSON objects with "tool" and "parameters" if (toolCalls.length === 0) { // Strategy: Find all potential JSON objects and validate them // Look for patterns that start with { and contain "tool" key const lines = text.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); // Check if line looks like a tool call (starts with { and contains "tool") if (line.startsWith('{') && line.includes('"tool"')) { try { // Try to parse the entire line as JSON const toolData = JSON.parse(line); if (toolData.tool && toolData.parameters && typeof toolData.parameters === 'object') { toolCalls.push(toolData); } } catch (e) { // If single line fails, try to find a complete JSON object across multiple lines let jsonStr = line; let braceCount = (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length; // Collect lines until braces are balanced let j = i + 1; while (braceCount > 0 && j < lines.length) { jsonStr += '\n' + lines[j]; braceCount += (lines[j].match(/\{/g) || []).length; braceCount -= (lines[j].match(/\}/g) || []).length; j++; } try { const toolData = JSON.parse(jsonStr); if (toolData.tool && toolData.parameters && typeof toolData.parameters === 'object') { toolCalls.push(toolData); i = j - 1; // Skip the lines we just processed } } catch (e2) { // Not a valid tool call, continue } } } } } console.log(`Parsed ${toolCalls.length} tool call(s):`, toolCalls); return toolCalls; } function removeToolCallsFromText(text) { // Remove XML-wrapped tool calls let cleaned = text.replace(/[\s\S]*?<\/tool_call>/g, '').trim(); // Also remove bare JSON tool calls by parsing the same way const lines = cleaned.split('\n'); const filteredLines = []; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); // Check if line looks like a tool call if (line.startsWith('{') && line.includes('"tool"')) { try { const toolData = JSON.parse(line); if (toolData.tool && toolData.parameters) { continue; // Skip this line, it's a tool call } } catch (e) { // Try multi-line parsing let jsonStr = line; let braceCount = (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length; let j = i + 1; while (braceCount > 0 && j < lines.length) { jsonStr += '\n' + lines[j]; braceCount += (lines[j].match(/\{/g) || []).length; braceCount -= (lines[j].match(/\}/g) || []).length; j++; } try { const toolData = JSON.parse(jsonStr); if (toolData.tool && toolData.parameters) { i = j - 1; // Skip all these lines continue; } } catch (e2) { // Not a tool call, keep the line } } } filteredLines.push(lines[i]); } return filteredLines.join('\n').trim(); } async function executeToolCall(toolCall) { const { tool, parameters } = toolCall; logAction(`Executing tool: ${tool}`, parameters); try { switch (tool) { case 'open_app': return await executeTool_OpenApp(parameters); case 'close_app': return await executeTool_CloseApp(parameters); case 'run_terminal_command': return await executeTool_RunTerminalCommand(parameters); case 'create_file': return await executeTool_CreateFile(parameters); case 'read_file': return await executeTool_ReadFile(parameters); case 'create_folder': return await executeTool_CreateFolder(parameters); case 'delete_item': return await executeTool_DeleteItem(parameters); case 'take_screenshot': return await executeTool_TakeScreenshot(parameters); case 'change_setting': return await executeTool_ChangeSetting(parameters); case 'list_apps': return await executeTool_ListApps(parameters); case 'list_files': return await executeTool_ListFiles(parameters); case 'get_system_info': return await executeTool_GetSystemInfo(parameters); case 'get_available_options': return await executeTool_GetAvailableOptions(parameters); default: return { success: false, message: `Unknown tool: ${tool}` }; } } catch (error) { return { success: false, message: `Error executing ${tool}: ${error.message}` }; } } // Tool execution functions function executeTool_OpenApp(params) { const { app_name } = params; openApp(app_name); return { success: true, message: `✅ Opened ${app_name} application`, details: `The ${app_name} app is now open and visible on your screen.` }; } function executeTool_CloseApp(params) { const { app_name } = params; const windows = document.querySelectorAll('.window'); let closed = false; windows.forEach(win => { const title = win.querySelector('.window-title')?.textContent?.toLowerCase(); if (title && title.includes(app_name.toLowerCase())) { win.remove(); closed = true; } }); if (closed) { return { success: true, message: `✅ Closed ${app_name} application`, details: `The ${app_name} window has been closed.` }; } else { return { success: false, message: `⚠️ Could not find open ${app_name} window`, details: `No window with that app name was found.` }; } } function executeTool_RunTerminalCommand(params) { const { command } = params; // Open terminal if not already open const terminalOpen = Array.from(document.querySelectorAll('.window')).some( win => win.querySelector('.window-title')?.textContent?.includes('Terminal') ); if (!terminalOpen) { openApp('terminal'); } // Wait a bit for terminal to open, then execute command setTimeout(() => { const terminalInput = document.querySelector('#terminalInput'); const terminal = document.getElementById('terminalContent'); if (terminalInput && terminal) { // Add command to terminal display const cmdLine = document.createElement('div'); cmdLine.className = 'terminal-line'; cmdLine.innerHTML = `user@nebulo:~$ ${command}`; terminal.insertBefore(cmdLine, terminal.lastElementChild); // Execute the command logic (simplified version) const output = document.createElement('div'); output.className = 'terminal-line'; if (command === 'help') { output.innerHTML = "Available commands:
help, ls, apps, themes, clear, date, whoami, reset-boot, echo"; } else if (command === 'date') { output.textContent = new Date().toString(); } else if (command === 'whoami') { output.textContent = localStorage.getItem('nOS_username') || 'user'; } else if (command === 'apps') { output.innerHTML = 'Installed Applications:
Files, Terminal, Browser, Games, Settings, Text Editor, Music, Photos, Calculator, App Store'; } else { output.textContent = `Executed: ${command}`; } terminal.insertBefore(output, terminal.lastElementChild); terminal.scrollTop = terminal.scrollHeight; } }, 300); return { success: true, message: `✅ Executed terminal command: ${command}`, details: `Command "${command}" was executed in the terminal.` }; } function executeTool_CreateFile(params) { const { path, content } = params; try { const parts = path.split('/').filter(p => p); const filename = parts.pop(); const folderPath = '/' + parts.join('/'); let currentLevel = fileSystem; for (const part of parts) { if (!currentLevel[part]) { currentLevel[part] = { type: 'folder', children: {} }; } currentLevel = currentLevel[part].children; } currentLevel[filename] = { type: 'file', content: content, modified: new Date().toISOString() }; saveFileSystem(); return { success: true, message: `✅ Created file: ${path}`, details: `File created with ${content.length} characters of content.` }; } catch (error) { return { success: false, message: `❌ Failed to create file: ${error.message}`, details: error.message }; } } function executeTool_ReadFile(params) { const { path } = params; try { const parts = path.split('/').filter(p => p); let currentLevel = fileSystem; for (const part of parts) { if (!currentLevel[part]) { throw new Error('File not found'); } if (currentLevel[part].type === 'folder') { currentLevel = currentLevel[part].children; } else { currentLevel = currentLevel[part]; break; } } if (currentLevel.type !== 'file') { throw new Error('Path is not a file'); } return { success: true, message: `✅ Read file: ${path}`, details: `File content:\n${currentLevel.content || '(empty file)'}` }; } catch (error) { return { success: false, message: `❌ Failed to read file: ${error.message}`, details: error.message }; } } function executeTool_CreateFolder(params) { const { path } = params; try { const parts = path.split('/').filter(p => p); let currentLevel = fileSystem; for (const part of parts) { if (!currentLevel[part]) { currentLevel[part] = { type: 'folder', children: {} }; } currentLevel = currentLevel[part].children; } saveFileSystem(); return { success: true, message: `✅ Created folder: ${path}`, details: `Folder created successfully.` }; } catch (error) { return { success: false, message: `❌ Failed to create folder: ${error.message}`, details: error.message }; } } function executeTool_DeleteItem(params) { const { path } = params; try { const parts = path.split('/').filter(p => p); const itemName = parts.pop(); let currentLevel = fileSystem; for (const part of parts) { if (!currentLevel[part]) { throw new Error('Path not found'); } currentLevel = currentLevel[part].children; } if (!currentLevel[itemName]) { throw new Error('Item not found'); } delete currentLevel[itemName]; saveFileSystem(); return { success: true, message: `✅ Deleted: ${path}`, details: `Item deleted successfully.` }; } catch (error) { return { success: false, message: `❌ Failed to delete item: ${error.message}`, details: error.message }; } } function executeTool_TakeScreenshot(params) { takeScreenshot(); return { success: true, message: `✅ Screenshot captured`, details: `Screenshot has been saved to your downloads.` }; } function executeTool_ChangeSetting(params) { const { setting, value } = params; try { // Theme setting if (setting === 'theme') { // Check if theme is installed if (!installedThemes.includes(value) && value !== 'dark') { return { success: false, message: `❌ Theme '${value}' is not installed`, details: `Available themes: dark, ${installedThemes.join(', ')}. Use get_available_options to see all themes.` }; } const themeLink = document.getElementById('themeLink'); if (themeLink) { themeLink.href = value === 'dark' ? '' : `/themes/${value}.css`; localStorage.setItem('nOS_selectedTheme', value); return { success: true, message: `✅ Changed theme to: ${value}`, details: `Theme has been applied successfully.` }; } } // Search engine setting if (setting === 'search_engine' || setting === 'searchEngine') { const searchEngines = { 'brave': 'https://search.brave.com/search?q=', 'duckduckgo': 'https://duckduckgo.com/?q=', 'google': 'https://www.google.com/search?q=', 'bing': 'https://www.bing.com/search?q=', 'startpage': 'https://www.startpage.com/search?q=', 'qwant': 'https://www.qwant.com/?q=' }; const engineUrl = searchEngines[value.toLowerCase()]; if (!engineUrl) { return { success: false, message: `❌ Unknown search engine: ${value}`, details: `Available: ${Object.keys(searchEngines).join(', ')}` }; } localStorage.setItem('nOS_searchEngine', engineUrl); return { success: true, message: `✅ Changed search engine to: ${value}`, details: `Default search engine updated.` }; } // Boolean settings (use12Hour, showSeconds, showDesktopIcons, showWhatsNew) const booleanSettings = { 'use12Hour': 'nOS_use12Hour', 'showSeconds': 'nOS_showSeconds', 'showDesktopIcons': 'nOS_showDesktopIcons', 'showWhatsNew': 'nebulo_showWhatsNew' }; if (booleanSettings[setting]) { const boolValue = value === 'true' || value === true; localStorage.setItem(booleanSettings[setting], boolValue.toString()); // Apply changes immediately if (setting === 'showDesktopIcons') { const icons = document.getElementById('desktopIcons'); if (icons) { icons.style.display = boolValue ? 'grid' : 'none'; } } return { success: true, message: `✅ Changed ${setting} to: ${boolValue}`, details: `Setting updated. Some changes may require reopening apps.` }; } // Wisp URL setting if (setting === 'wispUrl' || setting === 'wisp_url') { localStorage.setItem('nOS_wispUrl', value); return { success: true, message: `✅ Changed Wisp URL to: ${value}`, details: `Proxy configuration updated.` }; } // Bare URL setting if (setting === 'bareUrl' || setting === 'bare_url') { localStorage.setItem('nOS_bareUrl', value); return { success: true, message: `✅ Changed Bare URL to: ${value}`, details: `Proxy configuration updated.` }; } return { success: false, message: `❌ Unknown setting: ${setting}`, details: `Use get_available_options with category 'settings' to see all available settings.` }; } catch (error) { return { success: false, message: `❌ Failed to change setting: ${error.message}`, details: error.message }; } } function executeTool_ListApps(params) { const apps = Object.keys(appMetadata).map(key => { const app = appMetadata[key]; return `${app.name} (${app.preinstalled ? 'preinstalled' : 'installed'})`; }).join(', '); return { success: true, message: `✅ Listed all applications`, details: `Available apps: ${apps}` }; } function executeTool_ListFiles(params) { const { path = '/' } = params; try { const parts = path.split('/').filter(p => p); let currentLevel = fileSystem; for (const part of parts) { if (!currentLevel[part]) { throw new Error('Path not found'); } currentLevel = currentLevel[part].children; } const items = Object.keys(currentLevel).map(name => { const item = currentLevel[name]; return `${name} (${item.type})`; }).join(', '); return { success: true, message: `✅ Listed contents of ${path}`, details: `Contents: ${items || '(empty)'}` }; } catch (error) { return { success: false, message: `❌ Failed to list files: ${error.message}`, details: error.message }; } } function executeTool_GetSystemInfo(params) { const username = localStorage.getItem('nOS_username') || 'user'; const openWindows = document.querySelectorAll('.window').length; const theme = localStorage.getItem('nOS_selectedTheme') || 'default'; const info = { username, openWindows, theme, browser: navigator.userAgent.split(' ').pop(), timestamp: new Date().toLocaleString() }; return { success: true, message: `✅ Retrieved system information`, details: `User: ${info.username}\nOpen windows: ${info.openWindows}\nTheme: ${info.theme}\nBrowser: ${info.browser}\nTime: ${info.timestamp}` }; } function executeTool_GetAvailableOptions(params) { const { category = 'all' } = params; const options = { themes: { installed: ['dark', ...installedThemes], available: ['dark', 'light', 'blue', 'red', 'purple', 'golden', 'green', 'liquidGlass'], current: localStorage.getItem('nOS_selectedTheme') || 'dark', description: 'Available color themes for Nebulo. Themes must be installed from App Store before use.' }, search_engines: { available: ['brave', 'duckduckgo', 'google', 'bing', 'startpage', 'qwant'], current: localStorage.getItem('nOS_searchEngine') || 'https://search.brave.com/search?q=', description: 'Available search engines for the browser' }, settings: { boolean: { use12Hour: { current: localStorage.getItem('nOS_use12Hour') === 'true', description: 'Use 12-hour time format (true/false)', values: ['true', 'false'] }, showSeconds: { current: localStorage.getItem('nOS_showSeconds') === 'true', description: 'Show seconds in taskbar clock (true/false)', values: ['true', 'false'] }, showDesktopIcons: { current: localStorage.getItem('nOS_showDesktopIcons') !== 'false', description: 'Show icons on desktop (true/false)', values: ['true', 'false'] }, showWhatsNew: { current: localStorage.getItem('nebulo_showWhatsNew') !== 'false', description: 'Show What\'s New on startup (true/false)', values: ['true', 'false'] } }, urls: { wispUrl: { current: localStorage.getItem('nOS_wispUrl') || 'wss://wisp.rhw.one/', description: 'Wisp proxy URL for browser' }, bareUrl: { current: localStorage.getItem('nOS_bareUrl') || 'https://useclassplay.vercel.app/fq/', description: 'Bare proxy URL for browser' } }, description: 'System settings that can be changed' }, apps: { available: Object.keys(appMetadata).map(key => ({ id: key, name: appMetadata[key].name, preinstalled: appMetadata[key].preinstalled })), description: 'All available applications in Nebulo' } }; let result = ''; if (category === 'all') { result = JSON.stringify(options, null, 2); } else if (category === 'themes') { result = `THEMES:\n` + `Installed: ${options.themes.installed.join(', ')}\n` + `Available to install: ${options.themes.available.filter(t => !options.themes.installed.includes(t)).join(', ')}\n` + `Current: ${options.themes.current}\n` + `Note: To use a theme, it must first be installed from the App Store.`; } else if (category === 'search_engines') { result = `SEARCH ENGINES:\n` + `Available: ${options.search_engines.available.join(', ')}\n` + `Current: ${options.search_engines.current}`; } else if (category === 'settings') { result = `SETTINGS:\n\n` + `Boolean Settings:\n`; for (const [key, val] of Object.entries(options.settings.boolean)) { result += ` ${key}: ${val.description}\n Current: ${val.current}\n Values: ${val.values.join(', ')}\n`; } result += `\nURL Settings:\n`; for (const [key, val] of Object.entries(options.settings.urls)) { result += ` ${key}: ${val.description}\n Current: ${val.current}\n`; } } else if (category === 'apps') { result = `APPS (${options.apps.available.length} total):\n`; options.apps.available.forEach(app => { result += ` ${app.id}: ${app.name} (${app.preinstalled ? 'preinstalled' : 'installed'})\n`; }); } else { return { success: false, message: `❌ Unknown category: ${category}`, details: `Valid categories: themes, apps, search_engines, settings, all` }; } return { success: true, message: `✅ Retrieved available options for: ${category}`, details: result }; } function logAction(action, details) { const timestamp = new Date().toISOString(); nautilusAI.actionLog.push({ timestamp, action, details }); console.log(`[AI Action] ${action}`, details); } function showToolApprovalDialog(toolCall, onApprove, onReject) { const { tool, parameters } = toolCall; const toolDef = OS_AUTOMATION_TOOLS[tool]; const modal = document.createElement('div'); modal.style.cssText = ` position: fixed; inset: 0; background: rgba(0, 0, 0, 0.8); backdrop-filter: blur(10px); display: flex; align-items: center; justify-content: center; z-index: 999999; animation: fadeIn 0.2s ease; `; const dialog = document.createElement('div'); dialog.style.cssText = ` background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); border: 2px solid rgba(125, 211, 192, 0.3); border-radius: 16px; padding: 24px; max-width: 500px; width: 90%; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); animation: slideUp 0.3s ease; `; dialog.innerHTML = `

AI Action Approval Required

The AI wants to perform an action

Action: ${tool}
${toolDef ? toolDef.description : 'Execute operation'}
Parameters:
${Object.entries(parameters).map(([key, value]) => `
${key}: ${value}
` ).join('')}
`; modal.appendChild(dialog); document.body.appendChild(modal); document.getElementById('approveToolBtn').onclick = () => { modal.remove(); onApprove(); }; document.getElementById('rejectToolBtn').onclick = () => { modal.remove(); onReject(); }; // Add animations const style = document.createElement('style'); style.textContent = ` @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes slideUp { from { transform: translateY(30px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } `; document.head.appendChild(style); } function addToolFeedbackToChat(toolCall, result) { const chatContainer = document.getElementById('aiChatMessages'); if (!chatContainer) return; const feedbackDiv = document.createElement('div'); feedbackDiv.style.cssText = ` background: ${result.success ? 'rgba(125, 211, 192, 0.15)' : 'rgba(239, 68, 68, 0.15)'}; border: 1px solid ${result.success ? 'rgba(125, 211, 192, 0.4)' : 'rgba(239, 68, 68, 0.4)'}; border-radius: 10px; padding: 15px; margin: 10px 0; animation: slideIn 0.3s ease-out; `; feedbackDiv.innerHTML = `
Action Result
${result.message}
${result.details ? `
${result.details}
` : ''}
`; chatContainer.appendChild(feedbackDiv); chatContainer.scrollTop = chatContainer.scrollHeight; } // Note: Initialization happens in openApp when nebulo-ai is opened // Handle Enter key in textarea document.addEventListener('keydown', (e) => { const aiInput = document.getElementById('aiInput'); if (e.target === aiInput && e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendAiMessage(); } // Auto-resize textarea if (e.target === aiInput) { setTimeout(() => { aiInput.style.height = 'auto'; aiInput.style.height = Math.min(aiInput.scrollHeight, 150) + 'px'; }, 0); } const focusedElement = document.activeElement; const isTyping = focusedElement.tagName === 'INPUT' || focusedElement.tagName === 'TEXTAREA' || focusedElement.tagName === 'SELECT' || focusedElement.isContentEditable; if (isTyping || document.hidden) { return; } }); document.addEventListener('DOMContentLoaded', function () { let hoverTimeout; document.addEventListener('mouseover', function (e) { const desktopIcon = e.target.closest('.desktop-icon'); if (desktopIcon) { const span = desktopIcon.querySelector('span'); if (span) { hoverTimeout = setTimeout(() => { span.classList.add('expanded'); }, 1200); } } }); document.addEventListener('mouseout', function (e) { const desktopIcon = e.target.closest('.desktop-icon'); if (desktopIcon) { const span = desktopIcon.querySelector('span'); if (span) { clearTimeout(hoverTimeout); span.classList.remove('expanded'); } } }); reorderStartMenuApps(); }); // Proxy Settings Helpers function toggleSearchDropdown(trigger) { const options = trigger.nextElementSibling; options.classList.toggle('show'); // Close when clicking outside document.addEventListener('click', function (e) { if (!trigger.contains(e.target) && !options.contains(e.target)) { options.classList.remove('show'); } }, { once: true }); } function selectSearchEngine(name, url) { localStorage.setItem('nOS_searchEngine', url); const label = document.getElementById('currentSearchEngine'); if (label) label.textContent = name; showToast('Search engine set to ' + name, 'fa-check'); } function changeWispUrl(url) { localStorage.setItem('nOS_wispUrl', url); showToast('Wisp URL updated', 'fa-check'); } function resetWispUrl() { const defaultUrl = 'wss://wisp.rhw.one/'; localStorage.setItem('nOS_wispUrl', defaultUrl); const input = document.getElementById('wispUrlInput'); if (input) input.value = defaultUrl; showToast('Wisp URL reset to default', 'fa-undo'); } // ==================== WEB APP CREATOR FUNCTIONS ==================== function getCustomWebApps() { const appsJson = localStorage.getItem('nebulo_customWebApps'); return appsJson ? JSON.parse(appsJson) : []; } function saveCustomWebApps(apps) { localStorage.setItem('nebulo_customWebApps', JSON.stringify(apps)); } function createCustomWebApp() { const nameInput = document.getElementById('webAppName'); const urlInput = document.getElementById('webAppUrl'); const iconInput = document.getElementById('webAppIcon'); const desktopIconInput = document.getElementById('webAppDesktopIcon'); const name = nameInput.value.trim(); let url = urlInput.value.trim(); const icon = 'fas ' + (iconInput.value.trim() || 'fa-globe'); const addToDesktop = desktopIconInput.checked; if (!name || !url) { showToast('Please enter an app name and URL', 'fa-exclamation-circle'); return; } if (!url.startsWith('http://') && !url.startsWith('https://')) { url = 'https://' + url; } const appId = 'webapp_' + Date.now(); const newApp = { id: appId, name: name, url: url, icon: icon, created: Date.now() }; // Save to storage const apps = getCustomWebApps(); apps.push(newApp); saveCustomWebApps(apps); // Add to desktop if requested if (addToDesktop) { createDesktopIcon(appId, name, icon); saveDesktopIconOrder(); // Should enable persistence } showToast(`App "${name}" created successfully!`, 'fa-check'); // Reset inputs nameInput.value = ''; urlInput.value = ''; // Refresh list refreshCustomWebAppsList(); } function refreshCustomWebAppsList() { const listEl = document.getElementById('customWebAppsList'); if (!listEl) return; const apps = getCustomWebApps(); if (apps.length === 0) { listEl.innerHTML = '

No custom apps created yet.

'; return; } listEl.innerHTML = apps.map(app => `
${app.name}
${app.url}
`).join(''); } function deleteCustomWebApp(appId) { if (!confirm('Are you sure you want to delete this app?')) return; let apps = getCustomWebApps(); apps = apps.filter(a => a.id !== appId); saveCustomWebApps(apps); // Remove desktop icon if it exists const desktopIcon = document.querySelector(`.desktop-icon[data-app="${appId}"]`); if (desktopIcon) { desktopIcon.remove(); saveDesktopIconOrder(); // Update persistence } refreshCustomWebAppsList(); showToast('App deleted', 'fa-trash'); } function launchCustomWebApp(appId) { const apps = getCustomWebApps(); const app = apps.find(a => a.id === appId); if (!app) { showToast('App not found', 'fa-exclamation-circle'); return; } // Force open the app using the specialized launcher openCustomWebAppWindow(app); } function openCustomWebAppWindow(app) { // Use Scramjet proxy for seamless embedding like the main browser initScramjetProxy(); let proxyUrl = encodeScramjetUrl(app.url); if (!proxyUrl || proxyUrl === app.url) { console.warn("Scramjet not available, using direct URL"); proxyUrl = app.url; } const content = `
`; createWindow( app.name, app.icon, content, 1000, 700, app.id, true // noPadding ); } function handlePopularAppCardActivation(card) { if (!card) return; const payload = { id: card.dataset.appId, name: card.dataset.appName || "Popular App", url: card.dataset.appUrl, icon: card.dataset.appIcon || "fas fa-globe", }; if (!payload.url) return; launchProxyApp(payload); } function launchProxyApp(app) { if (!app || !app.url) return; initScramjetProxy(); let proxyUrl = encodeScramjetUrl(app.url); if (!proxyUrl || proxyUrl === app.url) { console.warn("[Popular Apps] Scramjet not available for", app.url, "using direct URL"); proxyUrl = app.url; } const content = `
`; showToast(`Launching ${app.name}`, "fa-arrow-up-right-from-square"); createWindow( app.name, app.icon || "fas fa-globe", content, 1000, 700, app.id, true ); } function initializeBrowserWispControls(windowEl) { if (!windowEl) return; const iframe = windowEl.querySelector("#browserUvIframe"); const wispUrl = window.currentScramjetWispUrl || resolvePreferredWispUrl(); persistWispUrl(wispUrl, { persist: wispUrl !== SCRAMJET_FALLBACK_WISP }); if (iframe && !iframe.src) { iframe.src = iframe.dataset.src || wispUrl; } } // Helper to create desktop icon dynamically function createDesktopIcon(appId, name, iconClass) { const desktopIcons = document.getElementById('desktopIcons'); if (!desktopIcons) return; // Check duplicate if (document.querySelector(`.desktop-icon[data-app="${appId}"]`)) return; const iconDiv = document.createElement('div'); iconDiv.className = 'desktop-icon'; if (appId === "browser" || appId === "games") { iconDiv.classList.add("desktop-icon-gold-ring"); } iconDiv.setAttribute('ondblclick', `openApp('${appId}')`); iconDiv.setAttribute('data-app', appId); iconDiv.innerHTML = ` ${name} `; desktopIcons.appendChild(iconDiv); // Re-init dragging if (typeof initDesktopIconDragging === 'function') { initDesktopIconDragging(); } } // Initialize custom apps on startup window.addEventListener('DOMContentLoaded', () => { const apps = getCustomWebApps(); apps.forEach(app => { appMetadata[app.id] = { name: app.name, icon: app.icon.replace('fas ', '').replace('fa-solid ', ''), preinstalled: false, type: 'custom-web-app' }; }); }); // ================= COMMUNITY STORE & UI IMPROVEMENTS ================= let communityAppsCache = null; // Global registry to store app data for safe onclick handling let communityAppRegistry = {}; async function fetchCommunityApps() { const container = document.getElementById('communityAppsGrid'); if (!container) return; // Not in community tab if (communityAppsCache) { renderCommunityApps(communityAppsCache); return; } try { // Fetch recursive tree const treeUrl = `https://api.github.com/repos/nautilus-os/community/git/trees/main?recursive=1&v=${Date.now()}`; const treeResp = await fetch(treeUrl); if (!treeResp.ok) throw new Error("Failed to fetch repository tree"); const treeData = await treeResp.json(); const infoFiles = treeData.tree.filter(node => node.path.endsWith('appinfo.json')); const fetchedItems = await Promise.all(infoFiles.map(async (node) => { try { // Path structure: [category]/[author]/[project]/appinfo.json const parts = node.path.split('/'); // Expect at least: category/author/project/appinfo.json (4 parts) if (parts.length < 4) return null; const category = parts[0]; // categories: apps, games, themes if (!['apps', 'games', 'themes'].includes(category)) return null; const cdnUrl = `https://cdn.jsdelivr.net/gh/nautilus-os/community@latest/${node.path}?v=${Date.now()}`; const metaResp = await fetch(cdnUrl); if (!metaResp.ok) return null; let metaArr = await metaResp.json(); if (!Array.isArray(metaArr)) metaArr = [metaArr]; // Handle if not array const meta = metaArr[0]; // Take first item if (!meta) return null; return { ...meta, category: category, // normalize category name for display if needed type: category === 'games' ? 'game' : (category === 'themes' ? 'theme' : 'app'), // Map img to icon if icon is missing icon: meta.img || meta.icon, isCommunity: true }; } catch (e) { console.error("Failed to process", node.path, e); return null; } })); const allItems = fetchedItems.filter(i => i !== null); communityAppsCache = allItems; renderCommunityApps(allItems); } catch (error) { console.error("Failed to fetch community apps:", error); container.innerHTML = `

Failed to load community apps. Check your connection.

`; } } function renderAppItem(app) { const isInstalled = app.isInstalled || false; // Generate a unique registry key for this app const registryKey = 'app_' + (app.name || 'unknown').toLowerCase().replace(/[^a-z0-9]/g, '_') + '_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); // Store app data in registry for safe retrieval communityAppRegistry[registryKey] = app; const iconHtml = app.customPreviewHtml ? `
${app.customPreviewHtml}
` : `
`; const installBtnText = app.installButtonText || (isInstalled ? 'Uninstall' : 'Install'); const installBtnDisabled = app.installButtonDisabled ? 'disabled style="opacity: 0.6; cursor: not-allowed;"' : ''; // Use registry key instead of inline JSON const installAction = isInstalled && app.uninstallAction ? app.uninstallAction : (app.installAction || `installCommunityAppByKey('${registryKey}')`); return `
${iconHtml}
${app.name}
${app.author || 'Unknown'}
${app.desc || 'No description provided.'}
`; } function renderCommunityApps(apps) { const container = document.getElementById('communityAppsGrid'); if (!container) return; if (apps.length === 0) { container.innerHTML = `
No community apps found. Please try again later.
`; return; } // Check installed status const installedCommunityApps = JSON.parse(localStorage.getItem('nebulo_communityApps') || '{}'); const installedThemesList = JSON.parse(localStorage.getItem('nebulo_installedThemes') || '[]'); const processedApps = apps.map(app => { const appId = app.name.toLowerCase().replace(/[^a-z0-9]/g, '-'); const isInstalled = (app.type === 'themes' || app.type === 'theme') ? installedThemesList.includes(app.name) : !!installedCommunityApps[appId]; return { ...app, isInstalled: isInstalled, installButtonText: isInstalled ? ((app.type && app.type.toLowerCase().includes('game')) ? 'Play' : 'Open') : 'Install', // Themes don't technically "open" in the same way, but we can set it to 'Installed' or disabled // actually, renderAppItem handles generic isInstalled. // For community apps specifically, if installed, action should be openApp(id) // Generate a unique registry key for this app registryKey: 'app_' + (app.name || 'unknown').toLowerCase().replace(/[^a-z0-9]/g, '_') + '_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9), installAction: isInstalled ? ((app.type === 'themes' || app.type === 'theme') ? "showToast('Theme is installed', 'fa-check')" : `openApp('${appId}')`) : null // Will be set after registry key is stored }; }); // Re-categorize processed apps const categories = {}; processedApps.forEach(app => { const cat = app.category || app.type || 'Other'; const displayCat = cat.charAt(0).toUpperCase() + cat.slice(1); if (!categories[displayCat]) categories[displayCat] = []; categories[displayCat].push(app); }); let html = ''; for (const [cat, items] of Object.entries(categories)) { html += `
${cat}
`; items.forEach(app => { html += renderAppItem(app); }); } container.innerHTML = html; } function viewAppDetails(app) { // Fullscreen/Modal view const modal = document.createElement('div'); modal.style.position = 'fixed'; modal.style.inset = '0'; modal.style.background = 'rgba(10, 14, 26, 0.95)'; modal.style.zIndex = '10000'; modal.style.display = 'flex'; modal.style.alignItems = 'center'; modal.style.justifyContent = 'center'; modal.style.padding = '1rem'; modal.style.animation = 'fadeIn 0.2s ease'; modal.onclick = (e) => { if (e.target === modal) modal.remove(); } const isInstalled = app.isInstalled || false; // Generate a unique registry key for this app (for modal use) const registryKey = 'modal_' + (app.name || 'unknown').toLowerCase().replace(/[^a-z0-9]/g, '_') + '_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); // Store app data in registry for safe retrieval communityAppRegistry[registryKey] = app; const iconHtml = app.customPreviewHtml ? `
${app.customPreviewHtml}
` : `
`; const installBtnText = app.installButtonText || (isInstalled ? 'Uninstall' : 'Install'); const installBtnDisabled = app.installButtonDisabled ? 'disabled style="opacity: 0.6; cursor: not-allowed;"' : ''; const installAction = isInstalled && app.uninstallAction ? app.uninstallAction : (app.installAction || `installCommunityAppByKey('${registryKey}')`); modal.innerHTML = `
${iconHtml}

${app.name}

${app.author || 'Unknown'} ${app.type ? `${app.type}` : ''}

${app.desc || 'No description available.'}

`; document.body.appendChild(modal); } // Helper function to install from registry key function installCommunityAppByKey(registryKey) { const app = communityAppRegistry[registryKey]; if (!app) { showToast('Error: App not found in registry', 'fa-exclamation-circle'); console.error('App not found for registry key:', registryKey); return; } installCommunityApp(app); } async function installCommunityApp(app) { showToast(`Installing ${app.name}...`, 'fa-download'); console.log(app.content) try { if (app.type === 'theme' || app.type === 'themes') { // Theme logic - assuming we just mark it as installed for now or would need to fetch CSS // The user prompt focuses on apps/games content installation. showToast(`${app.name} installed!`, 'fa-check'); if (!installedThemes.includes(app.name)) { installedThemes.push(app.name); localStorage.setItem("nebulo_installedThemes", JSON.stringify(installedThemes)); } refreshAppStore(); return; } // App/Game installation let content = ''; // If content is a path (starts with /), fetch it if (app.content && typeof app.content === 'string' && app.content.startsWith('/')) { const contentUrl = `https://cdn.jsdelivr.net/gh/nautilus-os/community@latest${app.content}`; const resp = await fetch(contentUrl); if (!resp.ok) throw new Error(`Failed to fetch content from ${contentUrl}`); content = await resp.text(); } else { // Fallback or raw content if it was somehow inline (though user said it is a file path) content = app.content || '
No content found.
'; } const appId = app.name.toLowerCase().replace(/[^a-z0-9]/g, '-'); // Save to localStorage const communityApps = JSON.parse(localStorage.getItem('nebulo_communityApps') || '{}'); communityApps[appId] = { name: app.name, icon: app.icon || app.img || 'fas fa-box', content: content, author: app.author, type: 'community-app' }; localStorage.setItem('nebulo_communityApps', JSON.stringify(communityApps)); // Create desktop icon createDesktopIcon(appId, app.name, app.icon || app.img || 'fas fa-box'); // Update global installedApps list just in case // Note: global installedApps array might not be persisted directly in all cases in this code base, // but usually checking `nebulo_installedApps` // We'll rely on our custom registry `nebulo_communityApps` showToast(`${app.name} installed!`, 'fa-check'); // Refresh UI to show "Uninstall" // We need to reload the metadata to reflect 'isInstalled' // This is tricky without a full reload, but refreshAppStore might help if it fetches again // For now, simple toast is good. refreshAppStore(); } catch (e) { console.error("Installation failed", e); showToast(`Failed to install ${app.name}`, 'fa-times'); } } // ================= DESKTOP SELECTION BOX ================= let selectionBox = { active: false, startX: 0, startY: 0, element: null }; function enableDesktopSelection() { const desktop = document.getElementById('desktop'); if (!desktop) return; // Create element if not exists if (!document.querySelector('.selection-box')) { const box = document.createElement('div'); box.className = 'selection-box'; document.body.appendChild(box); selectionBox.element = box; } else { selectionBox.element = document.querySelector('.selection-box'); } desktop.addEventListener('mousedown', (e) => { // Return if clicking taskbar, or if NOT clicking desktop/icons/wallpaper // Allow start drag if target is desktop (ID), desktop-icons (class), or wallpaper if (e.target.closest('.taskbar') || e.target.closest('.start-menu') || e.target.closest('.window')) return; // If e.target is desktop, wallpaper, or desktop-icons container, allow. // Assuming .desktop-icons has pointer-events: auto, clicking empty space in it returns .desktop-icons as target. const isDesktop = e.target.id === 'desktop'; const isWallpaper = e.target.closest('.wallpaper'); const isDesktopIcons = e.target.classList.contains('desktop-icons'); if (!isDesktop && !isWallpaper && !isDesktopIcons) return; selectionBox.active = true; selectionBox.startX = e.clientX; selectionBox.startY = e.clientY; selectionBox.element.style.left = e.clientX + 'px'; selectionBox.element.style.top = e.clientY + 'px'; selectionBox.element.style.width = '0px'; selectionBox.element.style.height = '0px'; selectionBox.element.style.display = 'block'; }); window.addEventListener('mousemove', (e) => { if (!selectionBox.active) return; const currentX = e.clientX; const currentY = e.clientY; const width = Math.abs(currentX - selectionBox.startX); const height = Math.abs(currentY - selectionBox.startY); const left = Math.min(currentX, selectionBox.startX); const top = Math.min(currentY, selectionBox.startY); selectionBox.element.style.width = width + 'px'; selectionBox.element.style.height = height + 'px'; selectionBox.element.style.left = left + 'px'; selectionBox.element.style.top = top + 'px'; }); window.addEventListener('mouseup', () => { if (selectionBox.active) { selectionBox.active = false; selectionBox.element.style.display = 'none'; } }); } // ================= TASKBAR SETTINGS ================= function updateTaskbarStyle() { const style = localStorage.getItem('nebulo_taskbarStyle') || 'floating'; const taskbar = document.getElementById('taskbar'); const startMenu = document.getElementById('startMenu'); const desktop = document.getElementById('desktop'); const systemTray = document.querySelector('.system-tray'); if (taskbar) { if (style === 'full') { taskbar.classList.add('full'); if (startMenu) startMenu.classList.add('full-mode'); if (desktop) desktop.classList.add('full-taskbar-offset'); if (systemTray) systemTray.classList.add('full-mode'); } else { taskbar.classList.remove('full'); if (startMenu) startMenu.classList.remove('full-mode'); if (desktop) desktop.classList.remove('full-taskbar-offset'); if (systemTray) systemTray.classList.remove('full-mode'); } } } function toggleTaskbarStyle() { const current = localStorage.getItem('nebulo_taskbarStyle') || 'floating'; const next = current === 'floating' ? 'full' : 'floating'; localStorage.setItem('nebulo_taskbarStyle', next); updateTaskbarStyle(); showToast(`Taskbar set to ${next}`, 'fa-cog'); } // Add taskbar setting to Context Menu (Right Click on Taskbar) // We'll hook into the global context menu or add a specific listener document.addEventListener('contextmenu', (e) => { if (e.target.closest('#taskbar')) { e.preventDefault(); const menu = document.getElementById('contextMenu'); menu.style.display = 'block'; menu.style.left = e.pageX + 'px'; menu.style.top = (e.pageY - 100) + 'px'; const style = localStorage.getItem('nebulo_taskbarStyle') || 'floating'; const nextLabel = style === 'floating' ? 'Switch to Full Mode' : 'Switch to Floating Mode'; menu.innerHTML = `
${nextLabel}
Taskbar Settings
`; // Close on click elsewhere (handled by existing main.js usually) } }); // Listen for messages from chat iframe window.addEventListener('message', function(event) { console.log('Main window received message from iframe:', event.data); if (event.data.type === 'updateChatBadge') { console.log('Processing updateChatBadge message'); const count = Number(event.data.count); if (Number.isFinite(count)) { updateGlobalChatBadge(count); } else { updateGlobalChatBadge(event.data.show !== false); } } else if (event.data.type === 'showChatNotification') { console.log('Processing showChatNotification message'); showChatToast(event.data.username, event.data.content); } else if (event.data.type === 'mentionUser') { const username = (event.data.username || '').trim(); if (username) { addUsersToMentionCache([{ username }]); } } else if (event.data.type === 'getOnlineUsers') { console.log('🔄 Processing getOnlineUsers request'); const onlineUsers = getOnlineUsers(); addUsersToMentionCache(onlineUsers); // Add avatar URLs to each user const usersWithAvatars = onlineUsers.map(user => ({ ...user, avatar: getUserProfilePicture(user.username) })); console.log('📤 Sending online users with avatars to chat iframe:', usersWithAvatars); // Send response back to iframe - try multiple selectors const selectors = [ '#global-chat-container iframe', '.window iframe[src*="chat-only.html"]', 'iframe[src*="chat-only.html"]' ]; let sent = false; for (const selector of selectors) { const iframes = document.querySelectorAll(selector); if (iframes.length > 0) { console.log(`✅ Found chat iframe with selector "${selector}"`); iframes.forEach(iframe => { if (iframe.contentWindow) { iframe.contentWindow.postMessage({ type: 'onlineUsers', users: usersWithAvatars }, '*'); console.log('✅ Sent online users with avatars to iframe'); sent = true; } }); break; } } if (!sent) { console.log('❌ No chat iframe found for getOnlineUsers request'); } } else if (event.data?.type === 'remoteAccounts') { const accounts = Array.isArray(event.data.accounts) ? event.data.accounts : []; console.log('📨 Received remote accounts list from chat iframe:', accounts.length); updateRemoteMentionAccounts(accounts); } else if (event.data.type === 'getUserAvatar') { console.log('Processing getUserAvatar request for:', event.data.username); const avatarUrl = getUserProfilePicture(event.data.username); // Send response back to iframe const iframes = document.querySelectorAll('#global-chat-container iframe'); iframes.forEach(iframe => { if (iframe.contentWindow) { iframe.contentWindow.postMessage({ type: 'userAvatar', username: event.data.username, avatar: avatarUrl }, '*'); } }); } }); // Clean up online statuses on startup cleanupOnlineStatuses(); // Mark user as offline when window closes window.addEventListener('beforeunload', function() { if (currentUsername && isSystemLoggedIn) { markUserOffline(currentUsername); } }); // Listen for localStorage changes from other tabs window.addEventListener('storage', function(event) { console.log('🔍 Storage event detected:', event.key, event.oldValue ? 'changed' : 'new'); if (event.key === 'nebulo_accounts') { console.log('📡 Accounts data changed in another tab, updating online users...'); console.log('📊 New accounts data length:', event.newValue ? event.newValue.length : 0); refreshMentionCacheFromAccounts(true); const iframes = document.querySelectorAll('iframe[src*="chat-only.html"]'); console.log('🔍 Found', iframes.length, 'chat iframes'); if (iframes.length > 0) { const onlineUsers = getOnlineUsers(); const usersWithAvatars = onlineUsers.map(user => ({ ...user, avatar: getUserProfilePicture(user.username) })); console.log('🔄 Cross-tab update: found', onlineUsers.length, 'online users:', onlineUsers.map(u => u.username)); iframes.forEach((iframe, i) => { if (iframe.contentWindow) { iframe.contentWindow.postMessage({ type: 'onlineUsers', users: usersWithAvatars }, '*'); console.log('📤 Sent updated online users to chat iframe', i, 'from storage event'); } else { console.log('❌ Chat iframe', i, 'has no contentWindow'); } }); } else { console.log('ℹ️ No chat iframes open, skipping update'); } } }); // ========== REAL-TIME USER ACTIVITY TRACKING ========== // Activity tracking configuration const ACTIVITY_TRACKING = { enabled: false, // Disabled by default - enable when you have a tracking server trackingUrl: 'https://your-tracking-server.com/api/ping', // Replace with your tracking endpoint pingInterval: 30000, // 30 seconds sessionId: null, lastActivity: Date.now() }; // Generate unique session ID function generateSessionId() { return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); } // Send activity ping to tracking server async function sendActivityPing() { if (!ACTIVITY_TRACKING.enabled) return; try { const pingData = { sessionId: ACTIVITY_TRACKING.sessionId, timestamp: Date.now(), userAgent: navigator.userAgent, url: window.location.href, referrer: document.referrer, screenSize: `${window.screen.width}x${window.screen.height}`, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, language: navigator.language, isLoggedIn: !!currentUsername, username: currentUsername || 'anonymous', lastActivity: ACTIVITY_TRACKING.lastActivity }; console.log('📡 Sending activity ping:', pingData); const response = await fetch(ACTIVITY_TRACKING.trackingUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(pingData), mode: 'cors', // Allow cross-origin requests keepalive: true // Allow request to complete even if page unloads }); if (response.ok) { console.log('✅ Activity ping sent successfully'); } else { console.log('⚠️ Activity ping failed:', response.status); } } catch (error) { console.log('❌ Activity ping error:', error.message); // Silently fail - don't spam console with tracking errors } } // Start activity tracking function startActivityTracking() { if (!ACTIVITY_TRACKING.enabled) { console.log('📊 Activity tracking disabled'); return; } // Generate session ID if not exists if (!ACTIVITY_TRACKING.sessionId) { ACTIVITY_TRACKING.sessionId = generateSessionId(); console.log('🆔 Generated session ID:', ACTIVITY_TRACKING.sessionId); } // Send initial ping sendActivityPing(); // Set up periodic pings setInterval(() => { sendActivityPing(); }, ACTIVITY_TRACKING.pingInterval); console.log(`⏰ Activity tracking started - pinging every ${ACTIVITY_TRACKING.pingInterval/1000} seconds`); // Track user activity const activityEvents = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click']; activityEvents.forEach(event => { document.addEventListener(event, () => { ACTIVITY_TRACKING.lastActivity = Date.now(); }, { passive: true }); }); // Send final ping when user leaves window.addEventListener('beforeunload', () => { // Send one final ping with unload flag navigator.sendBeacon(ACTIVITY_TRACKING.trackingUrl, JSON.stringify({ sessionId: ACTIVITY_TRACKING.sessionId, timestamp: Date.now(), event: 'unload', username: currentUsername || 'anonymous' })); }); } // Configure tracking server window.configureActivityTracking = function(options = {}) { if (options.enabled !== undefined) ACTIVITY_TRACKING.enabled = options.enabled; if (options.trackingUrl) ACTIVITY_TRACKING.trackingUrl = options.trackingUrl; if (options.pingInterval) ACTIVITY_TRACKING.pingInterval = options.pingInterval; console.log('⚙️ Activity tracking configured:', ACTIVITY_TRACKING); }; // Test activity tracking window.testActivityTracking = function() { console.log('🧪 Testing activity tracking...'); console.log('Session ID:', ACTIVITY_TRACKING.sessionId); console.log('Tracking enabled:', ACTIVITY_TRACKING.enabled); console.log('Last activity:', new Date(ACTIVITY_TRACKING.lastActivity).toLocaleTimeString()); // Send a test ping sendActivityPing(); }; // Demo: Configure for testing (replace with your actual server) // configureActivityTracking({ // trackingUrl: 'https://your-tracking-server.com/api/ping', // enabled: true // }); // Init new features enableDesktopSelection(); updateTaskbarStyle(); // Start activity tracking after login if (isSystemLoggedIn) { startActivityTracking(); } // Also start tracking when user logs in window.addEventListener('Login Success', () => { startActivityTracking(); }); // Start online indicators for anonymous users too if (!isSystemLoggedIn) { } // ========== CHAT SECURITY & INTEGRITY PROTECTION ========== // Protect global variables and monitor for DOM conflicts (function() { 'use strict'; console.log('[SECURITY] Initializing chat protection system'); // Store original window properties that are critical for chat const originalWindow = {}; const protectedProperties = ['chatMessages', 'chatInput', 'globalChatWindow']; protectedProperties.forEach(prop => { if (window[prop]) { originalWindow[prop] = window[prop]; } }); // Restore protected properties if they get overwritten by injected content const restoreProtectedProperties = () => { protectedProperties.forEach(prop => { if (originalWindow[prop] && !window[prop]) { console.warn(`[SECURITY] Restoring protected property: ${prop}`); window[prop] = originalWindow[prop]; } }); }; // Monitor DOM changes to detect malicious injection or unintended modification const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'childList') { const chatWindow = document.getElementById('globalChatWindow'); if (chatWindow && !chatWindow.classList.contains('open')) { // Check if properties were affected restoreProtectedProperties(); } } }); }); // Start observing the document body for changes observer.observe(document.body, { childList: true, subtree: true }); // Listen for page loaded messages from browser components to re-verify integrity window.addEventListener('message', (event) => { if (event.data && event.data.type === 'pageLoaded') { restoreProtectedProperties(); checkChatIntegrity(); } }); })(); // Diagnostic function to check for DOM conflicts and unexpected elements function checkChatIntegrity() { // console.log('[DEBUG] Checking chat integrity...'); const chatWindow = document.getElementById('globalChatWindow'); if (!chatWindow) return; // Check for browser elements that might have leaked into the chat container // This happens when viewEl.innerHTML is used incorrectly const leakedElements = chatWindow.querySelectorAll('.browser-view, .browser-error, iframe:not([id="chat-iframe"])'); if (leakedElements.length > 0) { console.error('[SECURITY] FOUND LEAKED BROWSER CONTENT IN CHAT WINDOW!'); leakedElements.forEach(el => { console.log('[SECURITY] Removing leaked element:', el.className || el.tagName); el.remove(); }); // If we had leaks, we should re-initialize the secure chat if (window.chatShadowRoot) { console.log('[SECURITY] Re-initializing secure chat after leak detection'); initSecureChat(); } } } // Run integrity check periodically setInterval(checkChatIntegrity, 5000); // Run check when window gains focus window.addEventListener('focus', checkChatIntegrity); let chatNotificationsEnabled = true; function toggleChatNotifications() { chatNotificationsEnabled = !chatNotificationsEnabled; localStorage.setItem('chatNotificationsEnabled', chatNotificationsEnabled); updateChatNotificationButton(); showToast(`Chat notifications ${chatNotificationsEnabled ? 'enabled' : 'disabled'}`, chatNotificationsEnabled ? 'fa-bell' : 'fa-bell-slash'); // Send updated notification state to chat iframe const chatIframe = document.getElementById('chat-iframe'); if (chatIframe && chatIframe.contentWindow) { chatIframe.contentWindow.postMessage({ type: 'toggleNotifications', enabled: chatNotificationsEnabled }, '*'); } } function updateChatNotificationButton() { const toggleButton = document.getElementById('chatNotificationToggle'); if (toggleButton) { if (chatNotificationsEnabled) { toggleButton.classList.remove('disabled'); toggleButton.title = "Disable Chat Notifications"; toggleButton.querySelector('i').className = 'fas fa-bell'; } else { toggleButton.classList.add('disabled'); toggleButton.title = "Enable Chat Notifications"; toggleButton.querySelector('i').className = 'fas fa-bell-slash'; } } } window.addEventListener('DOMContentLoaded', () => { if (localStorage.getItem('chatNotificationsEnabled') === 'false') { chatNotificationsEnabled = false; } updateChatNotificationButton(); const toggleButton = document.getElementById('chatNotificationToggle'); if (toggleButton) { toggleButton.addEventListener('click', toggleChatNotifications); } const desktopInput = document.getElementById('chatInput'); const desktopSuggestionBox = document.getElementById('mentionSuggestionsDesktop'); if (desktopInput && desktopSuggestionBox) { setupChatMentionSuggestions(desktopInput, desktopSuggestionBox); } });